riot-web/src/linkify-matrix.ts

234 lines
8.1 KiB
TypeScript
Raw Normal View History

/*
2016-01-07 05:06:39 +01:00
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as linkifyjs from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyString from 'linkifyjs/string';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
2021-06-29 14:11:58 +02:00
import { baseUrl } from "./utils/permalinks/SpecPermalinkConstructor";
import {
parsePermalink,
tryTransformEntityToPermalink,
tryTransformPermalinkToLocalHref,
} from "./utils/permalinks/Permalinks";
import dis from './dispatcher/dispatcher';
import { Action } from './dispatcher/actions';
import { ViewUserPayload } from './dispatcher/payloads/ViewUserPayload';
enum Type {
URL = "url",
UserId = "userid",
RoomAlias = "roomalias",
GroupId = "groupid"
}
// Linkifyjs types don't have parser, which really makes this harder.
const linkifyTokens = (linkifyjs as any).scanner.TOKENS;
enum MatrixLinkInitialToken {
POUND = linkifyTokens.POUND,
PLUS = linkifyTokens.PLUS,
AT = linkifyTokens.AT,
}
/**
* Token should be one of the type of linkify.parser.TOKENS[AT | PLUS | POUND]
* but due to typing issues it's just not a feasible solution.
* This problem kind of gets solved in linkify 3.0
*/
function parseFreeformMatrixLinks(linkify, token: MatrixLinkInitialToken, type: Type): void {
// Text tokens
const TT = linkify.scanner.TOKENS;
// Multi tokens
const MT = linkify.parser.TOKENS;
const MultiToken = MT.Base;
const S_START = linkify.parser.start;
const TOKEN = function(value) {
MultiToken.call(this, value);
this.type = type;
this.isLink = true;
};
TOKEN.prototype = new MultiToken();
const S_TOKEN = S_START.jump(token);
const S_TOKEN_NAME = new linkify.parser.State();
const S_TOKEN_NAME_COLON = new linkify.parser.State();
const S_TOKEN_NAME_COLON_DOMAIN = new linkify.parser.State(TOKEN);
const S_TOKEN_NAME_COLON_DOMAIN_DOT = new linkify.parser.State();
const S_MX_LINK = new linkify.parser.State(TOKEN);
const S_MX_LINK_COLON = new linkify.parser.State();
const S_MX_LINK_COLON_NUM = new linkify.parser.State(TOKEN);
const allowedFreeformTokens = [
TT.DOT,
TT.PLUS,
TT.NUM,
TT.DOMAIN,
TT.TLD,
TT.UNDERSCORE,
token,
// because 'localhost' is tokenised to the localhost token,
// usernames @localhost:foo.com are otherwise not matched!
TT.LOCALHOST,
];
S_TOKEN.on(allowedFreeformTokens, S_TOKEN_NAME);
S_TOKEN_NAME.on(allowedFreeformTokens, S_TOKEN_NAME);
S_TOKEN_NAME.on(TT.DOMAIN, S_TOKEN_NAME);
2017-06-05 17:51:50 +02:00
S_TOKEN_NAME.on(TT.COLON, S_TOKEN_NAME_COLON);
2017-06-05 17:51:50 +02:00
S_TOKEN_NAME_COLON.on(TT.DOMAIN, S_TOKEN_NAME_COLON_DOMAIN);
S_TOKEN_NAME_COLON.on(TT.LOCALHOST, S_MX_LINK); // accept #foo:localhost
S_TOKEN_NAME_COLON.on(TT.TLD, S_MX_LINK); // accept #foo:com (mostly for (TLD|DOMAIN)+ mixing)
S_TOKEN_NAME_COLON_DOMAIN.on(TT.DOT, S_TOKEN_NAME_COLON_DOMAIN_DOT);
S_TOKEN_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_TOKEN_NAME_COLON_DOMAIN);
S_TOKEN_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_MX_LINK);
2017-06-05 17:51:50 +02:00
S_MX_LINK.on(TT.DOT, S_TOKEN_NAME_COLON_DOMAIN_DOT); // accept repeated TLDs (e.g .org.uk)
S_MX_LINK.on(TT.COLON, S_MX_LINK_COLON); // do not accept trailing `:`
S_MX_LINK_COLON.on(TT.NUM, S_MX_LINK_COLON_NUM); // but do accept :NUM (port specifier)
}
function onUserClick(event: MouseEvent, userId: string) {
const member = new RoomMember(null, userId);
if (!member) { return; }
dis.dispatch<ViewUserPayload>({
action: Action.ViewUser,
member: member,
});
}
function onAliasClick(event: MouseEvent, roomAlias: string) {
event.preventDefault();
dis.dispatch({
action: Action.ViewRoom,
room_alias: roomAlias,
});
}
function onGroupClick(event: MouseEvent, groupId: string) {
event.preventDefault();
dis.dispatch({ action: 'view_group', group_id: groupId });
}
const escapeRegExp = function(string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
// Recognise URLs from both our local and official Element deployments.
// Anyone else really should be using matrix.to.
export const ELEMENT_URL_PATTERN =
"^(?:https?://)?(?:" +
escapeRegExp(window.location.host + window.location.pathname) + "|" +
"(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/|" +
"(?:app|beta|staging|develop)\\.element\\.io/" +
")(#.*)";
export const MATRIXTO_URL_PATTERN = "^(?:https?://)?(?:www\\.)?matrix\\.to/#/(([#@!+]).*)";
export const MATRIXTO_MD_LINK_PATTERN =
'\\[([^\\]]*)\\]\\((?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!+][^\\)]*)\\)';
export const MATRIXTO_BASE_URL= baseUrl;
export const options = {
events: function(href: string, type: Type | string): Partial<GlobalEventHandlers> {
switch (type) {
case Type.URL: {
// intercept local permalinks to users and show them like userids (in userinfo of current room)
try {
const permalink = parsePermalink(href);
if (permalink && permalink.userId) {
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
onUserClick(e, permalink.userId);
},
};
}
} catch (e) {
// OK fine, it's not actually a permalink
}
break;
}
case Type.UserId:
2015-10-27 10:58:55 +01:00
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
onUserClick(e, href);
},
2015-10-27 10:58:55 +01:00
};
case Type.RoomAlias:
2015-10-27 10:58:55 +01:00
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
onAliasClick(e, href);
},
2015-10-27 10:58:55 +01:00
};
case Type.GroupId:
2017-06-05 17:51:50 +02:00
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
onGroupClick(e, href);
},
2017-06-05 17:51:50 +02:00
};
}
},
formatHref: function(href: string, type: Type | string): string {
switch (type) {
case Type.RoomAlias:
case Type.UserId:
case Type.GroupId:
default: {
return tryTransformEntityToPermalink(href);
}
}
},
linkAttributes: {
rel: 'noreferrer noopener',
},
target: function(href: string, type: Type | string): string {
if (type === Type.URL) {
try {
const transformed = tryTransformPermalinkToLocalHref(href);
if (transformed !== href || decodeURIComponent(href).match(ELEMENT_URL_PATTERN)) {
return null;
} else {
return '_blank';
}
} catch (e) {
// malformed URI
}
}
2016-03-21 16:45:04 +01:00
return null;
},
};
// Run the plugins
// Linkify room aliases
parseFreeformMatrixLinks(linkifyjs, MatrixLinkInitialToken.POUND, Type.RoomAlias);
// Linkify group IDs
parseFreeformMatrixLinks(linkifyjs, MatrixLinkInitialToken.PLUS, Type.GroupId);
// Linkify user IDs
parseFreeformMatrixLinks(linkifyjs, MatrixLinkInitialToken.AT, Type.UserId);
export const linkify = linkifyjs;
export const _linkifyElement = linkifyElement;
export const _linkifyString = linkifyString;