Files
ThreadNet-Web/src/linkify-matrix.ts
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

279 lines
9.2 KiB
TypeScript
Raw Normal View History

/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
2024-09-09 14:57:16 +01:00
Copyright 2015, 2016 OpenMarket Ltd
2024-09-09 14:57:16 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
2022-02-04 17:21:03 +00:00
import * as linkifyjs from "linkifyjs";
import { EventListeners, Opts, registerCustomProtocol, registerPlugin } from "linkifyjs";
2022-02-04 17:21:03 +00:00
import linkifyElement from "linkify-element";
import linkifyString from "linkify-string";
import { getHttpUriForMxc, User } from "matrix-js-sdk/src/matrix";
2021-12-09 09:10:23 +00:00
import {
parsePermalink,
tryTransformEntityToPermalink,
2020-03-20 00:56:41 +00:00
tryTransformPermalinkToLocalHref,
} from "./utils/permalinks/Permalinks";
2021-12-03 15:00:56 +01:00
import dis from "./dispatcher/dispatcher";
import { Action } from "./dispatcher/actions";
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
2017-12-10 12:50:41 +00:00
2022-01-20 10:18:47 -07:00
export enum Type {
2021-09-27 15:24:44 +02:00
URL = "url",
UserId = "userid",
RoomAlias = "roomalias",
}
2022-01-18 18:24:16 +01:00
function matrixOpaqueIdLinkifyParser({
scanner,
parser,
token,
name,
}: {
2023-06-27 13:23:37 +02:00
scanner: linkifyjs.ScannerInit;
parser: linkifyjs.ParserInit;
2022-01-18 18:24:16 +01:00
token: "#" | "+" | "@";
name: Type;
}): void {
2022-01-18 18:24:16 +01:00
const {
DOT,
2022-01-20 10:18:47 -07:00
// IPV4 necessity
2022-01-18 18:24:16 +01:00
NUM,
COLON,
SYM,
SLASH,
EQUALS,
2022-01-20 10:18:47 -07:00
HYPHEN,
2022-01-18 18:24:16 +01:00
UNDERSCORE,
} = scanner.tokens;
2021-12-03 15:00:56 +01:00
2023-06-27 13:23:37 +02:00
// Contains NUM, WORD, UWORD, EMOJI, TLD, UTLD, SCHEME, SLASH_SCHEME and LOCALHOST plus custom protocols (e.g. "matrix")
const { domain } = scanner.tokens.groups;
2023-06-27 13:23:37 +02:00
// Tokens we need that are not contained in the domain group
const additionalLocalpartTokens = [DOT, SYM, SLASH, EQUALS, UNDERSCORE, HYPHEN];
const additionalDomainpartTokens = [HYPHEN];
2023-06-27 13:23:37 +02:00
const matrixToken = linkifyjs.createTokenClass(name, { isLink: true });
const matrixTokenState = new linkifyjs.State(matrixToken) as any as linkifyjs.State<linkifyjs.MultiToken>; // linkify doesn't appear to type this correctly
2023-06-27 13:23:37 +02:00
const matrixTokenWithPort = linkifyjs.createTokenClass(name, { isLink: true });
const matrixTokenWithPortState = new linkifyjs.State(
matrixTokenWithPort,
) as any as linkifyjs.State<linkifyjs.MultiToken>; // linkify doesn't appear to type this correctly
2023-06-27 13:23:37 +02:00
const INITIAL_STATE = parser.start.tt(token);
// Localpart
const LOCALPART_STATE = new linkifyjs.State<linkifyjs.MultiToken>();
INITIAL_STATE.ta(domain, LOCALPART_STATE);
INITIAL_STATE.ta(additionalLocalpartTokens, LOCALPART_STATE);
LOCALPART_STATE.ta(domain, LOCALPART_STATE);
LOCALPART_STATE.ta(additionalLocalpartTokens, LOCALPART_STATE);
// Domainpart
2022-01-18 18:24:16 +01:00
const DOMAINPART_STATE_DOT = LOCALPART_STATE.tt(COLON);
2023-06-27 13:23:37 +02:00
DOMAINPART_STATE_DOT.ta(domain, matrixTokenState);
DOMAINPART_STATE_DOT.ta(additionalDomainpartTokens, matrixTokenState);
matrixTokenState.ta(domain, matrixTokenState);
matrixTokenState.ta(additionalDomainpartTokens, matrixTokenState);
matrixTokenState.tt(DOT, DOMAINPART_STATE_DOT);
2023-06-27 13:23:37 +02:00
// Port suffixes
matrixTokenState.tt(COLON).tt(NUM, matrixTokenWithPortState);
}
function onUserClick(event: MouseEvent, userId: string): void {
2022-01-20 10:18:47 -07:00
event.preventDefault();
2021-12-03 15:00:56 +01:00
dis.dispatch<ViewUserPayload>({
action: Action.ViewUser,
member: new User(userId),
2021-12-03 15:00:56 +01:00
});
}
2022-01-20 10:18:47 -07:00
function onAliasClick(event: MouseEvent, roomAlias: string): void {
2021-12-03 15:00:56 +01:00
event.preventDefault();
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_alias: roomAlias,
metricsTrigger: "Timeline",
metricsViaKeyboard: false,
});
2021-12-03 15:00:56 +01:00
}
2022-01-20 10:18:47 -07:00
2022-08-09 12:55:49 +01:00
const escapeRegExp = function (s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
2020-12-21 12:46:29 +00:00
// Recognise URLs from both our local and official Element deployments.
// Anyone else really should be using matrix.to. vector:// allowed to support Element Desktop relative links.
2021-12-03 15:00:56 +01:00
export const ELEMENT_URL_PATTERN =
"^(?:vector://|https?://)?(?:" +
2020-12-21 12:46:29 +00:00
escapeRegExp(window.location.host + window.location.pathname) +
"|" +
"(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/|" +
"(?:app|beta|staging|develop)\\.element\\.io/" +
2020-12-21 12:46:29 +00:00
")(#.*)";
export const options: Opts = {
events: function (href: string, type: string): EventListeners {
switch (type as Type) {
2021-09-27 15:24:44 +02:00
case Type.URL: {
// intercept local permalinks to users and show them like userids (in userinfo of current room)
2020-03-26 10:45:26 +00:00
try {
const permalink = parsePermalink(href);
if (permalink?.userId) {
2020-03-26 10:45:26 +00:00
return {
click: function (e: MouseEvent) {
onUserClick(e, permalink.userId!);
2020-03-26 10:45:26 +00:00
},
};
2022-01-20 10:18:47 -07:00
} else {
// for events, rooms etc. (anything other than users)
2022-01-20 10:18:47 -07:00
const localHref = tryTransformPermalinkToLocalHref(href);
if (localHref !== href) {
// it could be converted to a localHref -> therefore handle locally
return {
click: function (e: MouseEvent) {
2022-01-20 10:18:47 -07:00
e.preventDefault();
window.location.hash = localHref;
},
};
}
2020-03-26 10:45:26 +00:00
}
} catch (e) {
// OK fine, it's not actually a permalink
}
break;
}
2021-09-27 15:24:44 +02:00
case Type.UserId:
2015-10-27 09:58:55 +00:00
return {
click: function (e: MouseEvent) {
const userId = parsePermalink(href)?.userId ?? href;
if (userId) onUserClick(e, userId);
2017-10-11 17:56:17 +01:00
},
2015-10-27 09:58:55 +00:00
};
2021-09-27 15:24:44 +02:00
case Type.RoomAlias:
2015-10-27 09:58:55 +00:00
return {
click: function (e: MouseEvent) {
const alias = parsePermalink(href)?.roomIdOrAlias ?? href;
if (alias) onAliasClick(e, alias);
2017-10-11 17:56:17 +01:00
},
2015-10-27 09:58:55 +00:00
};
}
return {};
},
formatHref: function (href: string, type: Type | string): string {
switch (type) {
case "url":
if (href.startsWith("mxc://") && MatrixClientPeg.get()) {
2024-02-11 08:32:57 +01:00
return getHttpUriForMxc(
MatrixClientPeg.get()!.baseUrl,
href,
undefined,
undefined,
undefined,
false,
true,
);
}
// fallthrough
2021-09-27 15:24:44 +02:00
case Type.RoomAlias:
case Type.UserId:
default: {
return tryTransformEntityToPermalink(MatrixClientPeg.safeGet(), href) ?? "";
}
}
},
2022-01-18 18:24:16 +01:00
attributes: {
rel: "noreferrer noopener",
},
2022-05-02 20:26:37 -04:00
ignoreTags: ["pre", "code"],
2022-01-18 18:24:16 +01:00
className: "linkified",
target: function (href: string, type: Type | string): string {
2021-09-27 15:24:44 +02:00
if (type === Type.URL) {
try {
const transformed = tryTransformPermalinkToLocalHref(href);
if (
transformed !== href || // if it could be converted to handle locally for matrix symbols e.g. @user:server.tdl and matrix.to
decodeURIComponent(href).match(ELEMENT_URL_PATTERN) // for https links to Element domains
2022-01-20 10:18:47 -07:00
) {
return "";
} else {
return "_blank";
}
} catch (e) {
// malformed URI
}
}
return "";
},
};
2021-12-03 15:00:56 +01:00
// Run the plugins
2023-06-27 13:23:37 +02:00
registerPlugin(Type.RoomAlias, ({ scanner, parser }) => {
2022-01-18 18:24:16 +01:00
const token = scanner.tokens.POUND as "#";
matrixOpaqueIdLinkifyParser({
2022-01-18 18:24:16 +01:00
scanner,
parser,
token,
name: Type.RoomAlias,
});
});
2023-06-27 13:23:37 +02:00
registerPlugin(Type.UserId, ({ scanner, parser }) => {
2022-01-18 18:24:16 +01:00
const token = scanner.tokens.AT as "@";
matrixOpaqueIdLinkifyParser({
2022-01-18 18:24:16 +01:00
scanner,
parser,
token,
name: Type.UserId,
});
});
2021-12-03 15:00:56 +01:00
// Linkify supports some common protocols but not others, register all permitted url schemes if unsupported
// https://github.com/Hypercontext/linkifyjs/blob/f4fad9df1870259622992bbfba38bfe3d0515609/packages/linkifyjs/src/scanner.js#L133-L141
// This also handles registering the `matrix:` protocol scheme
const linkifySupportedProtocols = ["file", "mailto", "http", "https", "ftp", "ftps"];
const optionalSlashProtocols = [
"bitcoin",
"geo",
"im",
"magnet",
"mailto",
"matrix",
"news",
"openpgp4fpr",
"sip",
"sms",
"smsto",
"tel",
"urn",
"xmpp",
];
PERMITTED_URL_SCHEMES.forEach((scheme) => {
if (!linkifySupportedProtocols.includes(scheme)) {
registerCustomProtocol(scheme, optionalSlashProtocols.includes(scheme));
}
});
2022-01-20 10:18:47 -07:00
registerCustomProtocol("mxc", false);
2021-12-03 15:00:56 +01:00
export const linkify = linkifyjs;
export const _linkifyElement = linkifyElement;
export const _linkifyString = linkifyString;