2015-07-10 21:37:13 +08:00
|
|
|
/*
|
2024-09-09 21:57:16 +08:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2019-10-01 10:17:54 +08:00
|
|
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
2024-09-09 21:57:16 +08:00
|
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
2015-07-10 21:37:13 +08:00
|
|
|
|
2024-09-09 21:57:16 +08:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
|
|
Please see LICENSE files in the repository root for full details.
|
2015-07-10 21:37:13 +08:00
|
|
|
*/
|
|
|
|
|
2022-02-05 01:21:03 +08:00
|
|
|
import * as linkifyjs from "linkifyjs";
|
2023-04-25 16:28:48 +08:00
|
|
|
import { EventListeners, Opts, registerCustomProtocol, registerPlugin } from "linkifyjs";
|
2022-02-05 01:21:03 +08:00
|
|
|
import linkifyElement from "linkify-element";
|
|
|
|
import linkifyString from "linkify-string";
|
2023-07-11 15:25:51 +08:00
|
|
|
import { getHttpUriForMxc, User } from "matrix-js-sdk/src/matrix";
|
2021-12-09 17:10:23 +08:00
|
|
|
|
2020-03-20 08:42:44 +08:00
|
|
|
import {
|
|
|
|
parsePermalink,
|
|
|
|
tryTransformEntityToPermalink,
|
2020-03-20 08:56:41 +08:00
|
|
|
tryTransformPermalinkToLocalHref,
|
2020-03-20 08:42:44 +08:00
|
|
|
} from "./utils/permalinks/Permalinks";
|
2021-12-03 22:00:56 +08:00
|
|
|
import dis from "./dispatcher/dispatcher";
|
|
|
|
import { Action } from "./dispatcher/actions";
|
|
|
|
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
|
2022-02-10 22:29:55 +08:00
|
|
|
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
2023-05-23 23:24:12 +08:00
|
|
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
2023-07-10 23:09:39 +08:00
|
|
|
import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
|
2017-12-10 20:50:41 +08:00
|
|
|
|
2022-01-21 01:18:47 +08:00
|
|
|
export enum Type {
|
2021-09-27 21:24:44 +08:00
|
|
|
URL = "url",
|
|
|
|
UserId = "userid",
|
|
|
|
RoomAlias = "roomalias",
|
|
|
|
}
|
|
|
|
|
2022-01-19 01:24:16 +08:00
|
|
|
function matrixOpaqueIdLinkifyParser({
|
|
|
|
scanner,
|
|
|
|
parser,
|
|
|
|
token,
|
|
|
|
name,
|
|
|
|
}: {
|
2023-06-27 19:23:37 +08:00
|
|
|
scanner: linkifyjs.ScannerInit;
|
|
|
|
parser: linkifyjs.ParserInit;
|
2022-01-19 01:24:16 +08:00
|
|
|
token: "#" | "+" | "@";
|
|
|
|
name: Type;
|
2023-01-12 21:25:14 +08:00
|
|
|
}): void {
|
2022-01-19 01:24:16 +08:00
|
|
|
const {
|
|
|
|
DOT,
|
2022-01-21 01:18:47 +08:00
|
|
|
// IPV4 necessity
|
2022-01-19 01:24:16 +08:00
|
|
|
NUM,
|
|
|
|
COLON,
|
|
|
|
SYM,
|
2023-05-09 15:46:19 +08:00
|
|
|
SLASH,
|
|
|
|
EQUALS,
|
2022-01-21 01:18:47 +08:00
|
|
|
HYPHEN,
|
2022-01-19 01:24:16 +08:00
|
|
|
UNDERSCORE,
|
|
|
|
} = scanner.tokens;
|
2021-12-03 22:00:56 +08:00
|
|
|
|
2023-06-27 19:23:37 +08:00
|
|
|
// Contains NUM, WORD, UWORD, EMOJI, TLD, UTLD, SCHEME, SLASH_SCHEME and LOCALHOST plus custom protocols (e.g. "matrix")
|
|
|
|
const { domain } = scanner.tokens.groups;
|
2022-01-19 01:24:16 +08:00
|
|
|
|
2023-06-27 19:23:37 +08:00
|
|
|
// Tokens we need that are not contained in the domain group
|
|
|
|
const additionalLocalpartTokens = [DOT, SYM, SLASH, EQUALS, UNDERSCORE, HYPHEN];
|
|
|
|
const additionalDomainpartTokens = [HYPHEN];
|
2022-01-19 01:24:16 +08:00
|
|
|
|
2023-06-27 19:23:37 +08: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
|
2015-07-10 21:37:13 +08:00
|
|
|
|
2023-06-27 19:23:37 +08: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
|
2017-06-05 23:51:50 +08:00
|
|
|
|
2023-06-27 19:23:37 +08:00
|
|
|
const INITIAL_STATE = parser.start.tt(token);
|
2017-06-05 23:51:50 +08:00
|
|
|
|
2023-06-27 19:23:37 +08:00
|
|
|
// 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);
|
2022-01-19 01:24:16 +08:00
|
|
|
|
2023-06-27 19:23:37 +08:00
|
|
|
// Domainpart
|
|
|
|
const DOMAINPART_STATE_DOT = LOCALPART_STATE.tt(COLON);
|
|
|
|
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);
|
|
|
|
|
|
|
|
// Port suffixes
|
|
|
|
matrixTokenState.tt(COLON).tt(NUM, matrixTokenWithPortState);
|
2015-07-10 21:37:13 +08:00
|
|
|
}
|
|
|
|
|
2023-01-12 21:25:14 +08:00
|
|
|
function onUserClick(event: MouseEvent, userId: string): void {
|
2022-01-21 01:18:47 +08:00
|
|
|
event.preventDefault();
|
2021-12-03 22:00:56 +08:00
|
|
|
dis.dispatch<ViewUserPayload>({
|
|
|
|
action: Action.ViewUser,
|
2023-02-17 01:21:44 +08:00
|
|
|
member: new User(userId),
|
2021-12-03 22:00:56 +08:00
|
|
|
});
|
|
|
|
}
|
2022-01-21 01:18:47 +08:00
|
|
|
|
2023-01-12 21:25:14 +08:00
|
|
|
function onAliasClick(event: MouseEvent, roomAlias: string): void {
|
2021-12-03 22:00:56 +08:00
|
|
|
event.preventDefault();
|
2022-02-10 22:29:55 +08:00
|
|
|
dis.dispatch<ViewRoomPayload>({
|
2022-01-13 04:12:28 +08:00
|
|
|
action: Action.ViewRoom,
|
|
|
|
room_alias: roomAlias,
|
2022-02-18 02:03:27 +08:00
|
|
|
metricsTrigger: "Timeline",
|
|
|
|
metricsViaKeyboard: false,
|
2022-01-13 04:12:28 +08:00
|
|
|
});
|
2021-12-03 22:00:56 +08:00
|
|
|
}
|
2022-01-21 01:18:47 +08:00
|
|
|
|
2022-08-09 19:55:49 +08:00
|
|
|
const escapeRegExp = function (s: string): string {
|
|
|
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
2016-03-20 20:31:30 +08:00
|
|
|
};
|
|
|
|
|
2020-12-21 20:46:29 +08:00
|
|
|
// Recognise URLs from both our local and official Element deployments.
|
2022-02-03 16:55:49 +08:00
|
|
|
// Anyone else really should be using matrix.to. vector:// allowed to support Element Desktop relative links.
|
2021-12-03 22:00:56 +08:00
|
|
|
export const ELEMENT_URL_PATTERN =
|
2022-02-03 16:55:49 +08:00
|
|
|
"^(?:vector://|https?://)?(?:" +
|
2020-12-21 20:46:29 +08:00
|
|
|
escapeRegExp(window.location.host + window.location.pathname) +
|
|
|
|
"|" +
|
2020-12-21 21:15:00 +08:00
|
|
|
"(?:www\\.)?(?:riot|vector)\\.im/(?:app|beta|staging|develop)/|" +
|
|
|
|
"(?:app|beta|staging|develop)\\.element\\.io/" +
|
2020-12-21 20:46:29 +08:00
|
|
|
")(#.*)";
|
2016-08-28 08:55:42 +08:00
|
|
|
|
2023-02-03 16:59:21 +08:00
|
|
|
export const options: Opts = {
|
2023-04-25 16:28:48 +08:00
|
|
|
events: function (href: string, type: string): EventListeners {
|
2023-02-03 16:59:21 +08:00
|
|
|
switch (type as Type) {
|
2021-09-27 21:24:44 +08:00
|
|
|
case Type.URL: {
|
2020-03-20 08:42:44 +08:00
|
|
|
// intercept local permalinks to users and show them like userids (in userinfo of current room)
|
2020-03-26 18:45:26 +08:00
|
|
|
try {
|
|
|
|
const permalink = parsePermalink(href);
|
2022-03-23 21:38:57 +08:00
|
|
|
if (permalink?.userId) {
|
2020-03-26 18:45:26 +08:00
|
|
|
return {
|
2022-03-23 21:38:57 +08:00
|
|
|
click: function (e: MouseEvent) {
|
2023-04-28 16:45:36 +08:00
|
|
|
onUserClick(e, permalink.userId!);
|
2020-03-26 18:45:26 +08:00
|
|
|
},
|
|
|
|
};
|
2022-01-21 01:18:47 +08:00
|
|
|
} else {
|
2022-03-23 21:38:57 +08:00
|
|
|
// for events, rooms etc. (anything other than users)
|
2022-01-21 01:18:47 +08:00
|
|
|
const localHref = tryTransformPermalinkToLocalHref(href);
|
|
|
|
if (localHref !== href) {
|
|
|
|
// it could be converted to a localHref -> therefore handle locally
|
|
|
|
return {
|
2022-03-23 21:38:57 +08:00
|
|
|
click: function (e: MouseEvent) {
|
2022-01-21 01:18:47 +08:00
|
|
|
e.preventDefault();
|
|
|
|
window.location.hash = localHref;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2020-03-26 18:45:26 +08:00
|
|
|
}
|
2024-10-16 23:43:07 +08:00
|
|
|
} catch {
|
2020-03-26 18:45:26 +08:00
|
|
|
// OK fine, it's not actually a permalink
|
2020-03-20 08:42:44 +08:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2021-09-27 21:24:44 +08:00
|
|
|
case Type.UserId:
|
2015-10-27 17:58:55 +08:00
|
|
|
return {
|
2022-03-23 21:38:57 +08:00
|
|
|
click: function (e: MouseEvent) {
|
2023-07-14 18:55:55 +08:00
|
|
|
const userId = parsePermalink(href)?.userId ?? href;
|
2023-02-24 23:28:40 +08:00
|
|
|
if (userId) onUserClick(e, userId);
|
2017-10-12 00:56:17 +08:00
|
|
|
},
|
2015-10-27 17:58:55 +08:00
|
|
|
};
|
2021-09-27 21:24:44 +08:00
|
|
|
case Type.RoomAlias:
|
2015-10-27 17:58:55 +08:00
|
|
|
return {
|
2022-03-23 21:38:57 +08:00
|
|
|
click: function (e: MouseEvent) {
|
2023-07-14 18:55:55 +08:00
|
|
|
const alias = parsePermalink(href)?.roomIdOrAlias ?? href;
|
2023-02-24 23:28:40 +08:00
|
|
|
if (alias) onAliasClick(e, alias);
|
2017-10-12 00:56:17 +08:00
|
|
|
},
|
2015-10-27 17:58:55 +08:00
|
|
|
};
|
2015-07-10 21:37:13 +08:00
|
|
|
}
|
2023-04-25 16:28:48 +08:00
|
|
|
|
|
|
|
return {};
|
2016-02-19 02:16:39 +08:00
|
|
|
},
|
2016-08-28 09:05:23 +08:00
|
|
|
|
2023-05-10 01:24:40 +08:00
|
|
|
formatHref: function (href: string, type: Type | string): string {
|
2016-02-19 02:16:39 +08:00
|
|
|
switch (type) {
|
2023-07-11 15:25:51 +08:00
|
|
|
case "url":
|
|
|
|
if (href.startsWith("mxc://") && MatrixClientPeg.get()) {
|
2024-02-11 15:32:57 +08:00
|
|
|
return getHttpUriForMxc(
|
|
|
|
MatrixClientPeg.get()!.baseUrl,
|
|
|
|
href,
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
undefined,
|
|
|
|
false,
|
|
|
|
true,
|
|
|
|
);
|
2023-07-11 15:25:51 +08:00
|
|
|
}
|
|
|
|
// fallthrough
|
2021-09-27 21:24:44 +08:00
|
|
|
case Type.RoomAlias:
|
|
|
|
case Type.UserId:
|
2018-06-16 15:27:17 +08:00
|
|
|
default: {
|
2023-06-22 00:29:44 +08:00
|
|
|
return tryTransformEntityToPermalink(MatrixClientPeg.safeGet(), href) ?? "";
|
2018-06-16 15:27:17 +08:00
|
|
|
}
|
2016-03-20 11:05:07 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2022-01-19 01:24:16 +08:00
|
|
|
attributes: {
|
2020-02-24 06:14:29 +08:00
|
|
|
rel: "noreferrer noopener",
|
2016-08-16 04:37:26 +08:00
|
|
|
},
|
|
|
|
|
2022-05-03 08:26:37 +08:00
|
|
|
ignoreTags: ["pre", "code"],
|
|
|
|
|
2022-01-19 01:24:16 +08:00
|
|
|
className: "linkified",
|
|
|
|
|
2023-05-10 01:24:40 +08:00
|
|
|
target: function (href: string, type: Type | string): string {
|
2021-09-27 21:24:44 +08:00
|
|
|
if (type === Type.URL) {
|
2021-05-13 17:20:27 +08:00
|
|
|
try {
|
|
|
|
const transformed = tryTransformPermalinkToLocalHref(href);
|
2022-01-25 22:31:00 +08:00
|
|
|
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-21 01:18:47 +08:00
|
|
|
) {
|
2023-05-10 01:24:40 +08:00
|
|
|
return "";
|
2021-05-13 17:20:27 +08:00
|
|
|
} else {
|
|
|
|
return "_blank";
|
|
|
|
}
|
2024-10-16 23:43:07 +08:00
|
|
|
} catch {
|
2021-05-13 17:20:27 +08:00
|
|
|
// malformed URI
|
2016-03-20 11:05:07 +08:00
|
|
|
}
|
|
|
|
}
|
2023-05-10 01:24:40 +08:00
|
|
|
return "";
|
2016-03-20 11:05:07 +08:00
|
|
|
},
|
2015-10-27 01:59:49 +08:00
|
|
|
};
|
2015-07-10 21:37:13 +08:00
|
|
|
|
2021-12-03 22:00:56 +08:00
|
|
|
// Run the plugins
|
2023-06-27 19:23:37 +08:00
|
|
|
registerPlugin(Type.RoomAlias, ({ scanner, parser }) => {
|
2022-01-19 01:24:16 +08:00
|
|
|
const token = scanner.tokens.POUND as "#";
|
2022-01-26 01:37:54 +08:00
|
|
|
matrixOpaqueIdLinkifyParser({
|
2022-01-19 01:24:16 +08:00
|
|
|
scanner,
|
|
|
|
parser,
|
|
|
|
token,
|
|
|
|
name: Type.RoomAlias,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-06-27 19:23:37 +08:00
|
|
|
registerPlugin(Type.UserId, ({ scanner, parser }) => {
|
2022-01-19 01:24:16 +08:00
|
|
|
const token = scanner.tokens.AT as "@";
|
2022-01-26 01:37:54 +08:00
|
|
|
matrixOpaqueIdLinkifyParser({
|
2022-01-19 01:24:16 +08:00
|
|
|
scanner,
|
|
|
|
parser,
|
|
|
|
token,
|
|
|
|
name: Type.UserId,
|
|
|
|
});
|
|
|
|
});
|
2021-12-03 22:00:56 +08:00
|
|
|
|
2023-07-10 23:09:39 +08: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-21 01:18:47 +08:00
|
|
|
|
2023-07-11 15:25:51 +08:00
|
|
|
registerCustomProtocol("mxc", false);
|
|
|
|
|
2021-12-03 22:00:56 +08:00
|
|
|
export const linkify = linkifyjs;
|
|
|
|
export const _linkifyElement = linkifyElement;
|
|
|
|
export const _linkifyString = linkifyString;
|