diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js
index 6ede36ee81..7a212b2497 100644
--- a/src/HtmlUtils.js
+++ b/src/HtmlUtils.js
@@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
+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.
@@ -33,6 +34,7 @@ import url from 'url';
import EMOJIBASE from 'emojibase-data/en/compact.json';
import EMOJIBASE_REGEX from 'emojibase-regex';
+import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
linkifyMatrix(linkify);
@@ -158,30 +160,10 @@ const transformTags = { // custom to matrix
if (attribs.href) {
attribs.target = '_blank'; // by default
- let m;
- // FIXME: horrible duplication with linkify-matrix
- m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
- if (m) {
- attribs.href = m[1];
+ const transformed = tryTransformPermalinkToLocalHref(attribs.href);
+ if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN)) {
+ attribs.href = transformed;
delete attribs.target;
- } else {
- m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
- if (m) {
- const entity = m[1];
- switch (entity[0]) {
- case '@':
- attribs.href = '#/user/' + entity;
- break;
- case '+':
- attribs.href = '#/group/' + entity;
- break;
- case '#':
- case '!':
- attribs.href = '#/room/' + entity;
- break;
- }
- delete attribs.target;
- }
}
}
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
@@ -465,10 +447,12 @@ export function bodyToHtml(content, highlights, opts={}) {
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length &&
// Prevent user pills expanding for users with only emoji in
- // their username
+ // their username. Permalinks (links in pills) can be any URL
+ // now, so we just check for an HTTP-looking thing.
(
content.formatted_body == undefined ||
- !content.formatted_body.includes("https://matrix.to/")
+ (!content.formatted_body.includes("http:") &&
+ !content.formatted_body.includes("https:"))
);
}
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index 21c837030b..d9fe28cc6d 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -23,8 +23,6 @@ import dis from './dispatcher';
import sdk from './index';
import {_t, _td} from './languageHandler';
import Modal from './Modal';
-import {MATRIXTO_URL_PATTERN} from "./linkify-matrix";
-import * as querystring from "querystring";
import MultiInviter from './utils/MultiInviter';
import { linkifyAndSanitizeHtml } from './HtmlUtils';
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
@@ -34,6 +32,7 @@ import Promise from "bluebird";
import { getAddressType } from './UserAddress';
import { abbreviateUrl } from './utils/UrlUtils';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
+import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks";
const singleMxcUpload = async () => {
return new Promise((resolve) => {
@@ -441,7 +440,19 @@ export const CommandMap = {
const params = args.split(' ');
if (params.length < 1) return reject(this.getUsage());
- const matrixToMatches = params[0].match(MATRIXTO_URL_PATTERN);
+ let isPermalink = false;
+ if (params[0].startsWith("http:") || params[0].startsWith("https:")) {
+ // It's at least a URL - try and pull out a hostname to check against the
+ // permalink handler
+ const parsedUrl = new URL(params[0]);
+ const hostname = parsedUrl.host || parsedUrl.hostname; // takes first non-falsey value
+
+ // if we're using a Riot permalink handler, this will catch it before we get much further.
+ // see below where we make assumptions about parsing the URL.
+ if (isPermalinkHost(hostname)) {
+ isPermalink = true;
+ }
+ }
if (params[0][0] === '#') {
let roomAlias = params[0];
if (!roomAlias.includes(':')) {
@@ -469,29 +480,25 @@ export const CommandMap = {
auto_join: true,
});
return success();
- } else if (matrixToMatches) {
- let entity = matrixToMatches[1];
- let eventId = null;
- let viaServers = [];
+ } else if (isPermalink) {
+ const permalinkParts = parsePermalink(params[0]);
- if (entity[0] !== '!' && entity[0] !== '#') return reject(this.getUsage());
-
- if (entity.indexOf('?') !== -1) {
- const parts = entity.split('?');
- entity = parts[0];
-
- const parsed = querystring.parse(parts[1]);
- viaServers = parsed["via"];
- if (typeof viaServers === 'string') viaServers = [viaServers];
+ // This check technically isn't needed because we already did our
+ // safety checks up above. However, for good measure, let's be sure.
+ if (!permalinkParts) {
+ return reject(this.getUsage());
}
- // We quietly support event ID permalinks too
- if (entity.indexOf('/$') !== -1) {
- const parts = entity.split("/$");
- entity = parts[0];
- eventId = `$${parts[1]}`;
+ // If for some reason someone wanted to join a group or user, we should
+ // stop them now.
+ if (!permalinkParts.roomIdOrAlias) {
+ return reject(this.getUsage());
}
+ const entity = permalinkParts.roomIdOrAlias;
+ const viaServers = permalinkParts.viaServers;
+ const eventId = permalinkParts.eventId;
+
const dispatch = {
action: 'view_room',
auto_join: true,
diff --git a/src/autocomplete/CommunityProvider.js b/src/autocomplete/CommunityProvider.js
index 992df0f773..0acfd426fb 100644
--- a/src/autocomplete/CommunityProvider.js
+++ b/src/autocomplete/CommunityProvider.js
@@ -23,7 +23,7 @@ import QueryMatcher from './QueryMatcher';
import {PillCompletion} from './Components';
import sdk from '../index';
import _sortBy from 'lodash/sortBy';
-import {makeGroupPermalink} from "../matrix-to";
+import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
import type {Completion, SelectionRange} from "./Autocompleter";
import FlairStore from "../stores/FlairStore";
diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js
index 79986657b8..b67abc388e 100644
--- a/src/autocomplete/RoomProvider.js
+++ b/src/autocomplete/RoomProvider.js
@@ -26,7 +26,7 @@ import {PillCompletion} from './Components';
import {getDisplayAliasForRoom} from '../Rooms';
import sdk from '../index';
import _sortBy from 'lodash/sortBy';
-import {makeRoomPermalink} from "../matrix-to";
+import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
import type {Completion, SelectionRange} from "./Autocompleter";
const ROOM_REGEX = /\B#\S*/g;
diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js
index 451ae0bb83..ac159c8213 100644
--- a/src/autocomplete/UserProvider.js
+++ b/src/autocomplete/UserProvider.js
@@ -28,7 +28,7 @@ import _sortBy from 'lodash/sortBy';
import MatrixClientPeg from '../MatrixClientPeg';
import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk';
-import {makeUserPermalink} from "../matrix-to";
+import {makeUserPermalink} from "../utils/permalinks/Permalinks";
import type {Completion, SelectionRange} from "./Autocompleter";
const USER_REGEX = /\B@\S*/g;
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 70d8b2e298..dd520bb7d4 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -36,7 +36,7 @@ import classnames from 'classnames';
import GroupStore from '../../stores/GroupStore';
import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
-import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
+import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
import {Group} from "matrix-js-sdk";
const LONG_DESC_PLACEHOLDER = _td(
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 3446901331..4d52158dae 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -31,7 +31,7 @@ import Promise from 'bluebird';
import classNames from 'classnames';
import {Room} from "matrix-js-sdk";
import { _t } from '../../languageHandler';
-import {RoomPermalinkCreator} from '../../matrix-to';
+import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
import MatrixClientPeg from '../../MatrixClientPeg';
import ContentMessages from '../../ContentMessages';
diff --git a/src/components/views/dialogs/ShareDialog.js b/src/components/views/dialogs/ShareDialog.js
index bd6746a1e5..f6d5b65fd6 100644
--- a/src/components/views/dialogs/ShareDialog.js
+++ b/src/components/views/dialogs/ShareDialog.js
@@ -20,7 +20,7 @@ import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import QRCode from 'qrcode-react';
-import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../matrix-to";
+import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import * as ContextualMenu from "../../structures/ContextualMenu";
const socials = [
diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index 4c987a0095..12830488b1 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector 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.
@@ -22,23 +23,21 @@ import classNames from 'classnames';
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
-import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
import { getDisplayAliasForRoom } from '../../../Rooms';
import FlairStore from "../../../stores/FlairStore";
-
-const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
+import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
-const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/;
+const REGEX_LOCAL_PERMALINK = /^#\/(?:user|room|group)\/(([#!@+])[^/]*)$/;
const Pill = createReactClass({
statics: {
isPillUrl: (url) => {
- return !!REGEX_MATRIXTO.exec(url);
+ return !!getPrimaryPermalinkEntity(url);
},
isMessagePillUrl: (url) => {
- return !!REGEX_LOCAL_MATRIXTO.exec(url);
+ return !!REGEX_LOCAL_PERMALINK.exec(url);
},
roomNotifPos: (text) => {
return text.indexOf("@room");
@@ -95,22 +94,21 @@ const Pill = createReactClass({
},
async componentWillReceiveProps(nextProps) {
- let regex = REGEX_MATRIXTO;
- if (nextProps.inMessage) {
- regex = REGEX_LOCAL_MATRIXTO;
- }
-
- let matrixToMatch;
let resourceId;
let prefix;
if (nextProps.url) {
- // Default to the empty array if no match for simplicity
- // resource and prefix will be undefined instead of throwing
- matrixToMatch = regex.exec(nextProps.url) || [];
+ if (nextProps.inMessage) {
+ // Default to the empty array if no match for simplicity
+ // resource and prefix will be undefined instead of throwing
+ const matrixToMatch = REGEX_LOCAL_PERMALINK.exec(nextProps.url) || [];
- resourceId = matrixToMatch[1]; // The room/user ID
- prefix = matrixToMatch[2]; // The first character of prefix
+ resourceId = matrixToMatch[1]; // The room/user ID
+ prefix = matrixToMatch[2]; // The first character of prefix
+ } else {
+ resourceId = getPrimaryPermalinkEntity(nextProps.url);
+ prefix = resourceId ? resourceId[0] : undefined;
+ }
}
const pillType = this.props.type || {
diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js
index 08630a16a5..fac0a71617 100644
--- a/src/components/views/elements/ReplyThread.js
+++ b/src/components/views/elements/ReplyThread.js
@@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
import dis from '../../../dispatcher';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
-import {makeUserPermalink, RoomPermalinkCreator} from "../../../matrix-to";
+import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
// This component does no cycle detection, simply because the only way to make such a cycle would be to
diff --git a/src/components/views/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.js
index bf0ef32460..9bb6fcc0d8 100644
--- a/src/components/views/messages/RoomCreate.js
+++ b/src/components/views/messages/RoomCreate.js
@@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher';
-import { RoomPermalinkCreator } from '../../../matrix-to';
+import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 7143d02e74..a9e0da143e 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -30,9 +30,9 @@ import { _t } from '../../../languageHandler';
import * as ContextualMenu from '../../structures/ContextualMenu';
import SettingsStore from "../../../settings/SettingsStore";
import ReplyThread from "../elements/ReplyThread";
-import {host as matrixtoHost} from '../../../matrix-to';
import {pillifyLinks} from '../../../utils/pillify';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
+import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
module.exports = createReactClass({
displayName: 'TextualBody',
@@ -248,10 +248,10 @@ module.exports = createReactClass({
const url = node.getAttribute("href");
const host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1];
- // never preview matrix.to links (if anything we should give a smart
+ // never preview permalinks (if anything we should give a smart
// preview of the room/user they point to: nobody needs to be reminded
// what the matrix.to site looks like).
- if (host === matrixtoHost) return false;
+ if (isPermalinkHost(host)) return false;
if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) {
// it's a "foo.pl" style link
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 022d45e60e..632ca53f82 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -23,7 +23,7 @@ import sdk from '../../../index';
import dis from '../../../dispatcher';
import RoomViewStore from '../../../stores/RoomViewStore';
import Stickerpicker from './Stickerpicker';
-import { makeRoomPermalink } from '../../../matrix-to';
+import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages';
import classNames from 'classnames';
import E2EIcon from './E2EIcon';
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index df7ba27493..cc92f7c750 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -48,13 +48,11 @@ import Markdown from '../../../Markdown';
import MessageComposerStore from '../../../stores/MessageComposerStore';
import ContentMessages from '../../../ContentMessages';
-import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
-
import EMOJIBASE from 'emojibase-data/en/compact.json';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
-import {makeUserPermalink} from "../../../matrix-to";
+import {getPrimaryPermalinkEntity, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import ReplyPreview from "./ReplyPreview";
import RoomViewStore from '../../../stores/RoomViewStore';
import ReplyThread from "../elements/ReplyThread";
@@ -224,18 +222,15 @@ export default class MessageComposerInput extends React.Component {
// special case links
if (tag === 'a') {
const href = el.getAttribute('href');
- let m;
- if (href) {
- m = href.match(MATRIXTO_URL_PATTERN);
- }
- if (m) {
+ const permalinkEntity = getPrimaryPermalinkEntity(href);
+ if (permalinkEntity) {
return {
object: 'inline',
type: 'pill',
data: {
href,
completion: el.innerText,
- completionId: m[1],
+ completionId: permalinkEntity,
},
};
} else {
@@ -541,7 +536,7 @@ export default class MessageComposerInput extends React.Component {
const textWithMdPills = this.plainWithMdPills.serialize(editorState);
const markdown = new Markdown(textWithMdPills);
- // HTML deserialize has custom rules to turn matrix.to links into pill objects.
+ // HTML deserialize has custom rules to turn permalinks into pill objects.
return this.html.deserialize(markdown.toHTML());
}
diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js
index 58e7237801..caf8feeea2 100644
--- a/src/components/views/rooms/ReplyPreview.js
+++ b/src/components/views/rooms/ReplyPreview.js
@@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
import PropTypes from "prop-types";
-import {RoomPermalinkCreator} from "../../../matrix-to";
+import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
function cancelQuoting() {
dis.dispatch({
diff --git a/src/components/views/rooms/SlateMessageComposer.js b/src/components/views/rooms/SlateMessageComposer.js
index d7aa745753..4bb2f29e61 100644
--- a/src/components/views/rooms/SlateMessageComposer.js
+++ b/src/components/views/rooms/SlateMessageComposer.js
@@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpicker from './Stickerpicker';
-import { makeRoomPermalink } from '../../../matrix-to';
+import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages';
import classNames from 'classnames';
diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js
index 04edd4541c..d41e413dbc 100644
--- a/src/editor/deserialize.js
+++ b/src/editor/deserialize.js
@@ -15,11 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
import { walkDOMDepthFirst } from "./dom";
import { checkBlockNode } from "../HtmlUtils";
-
-const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
+import {getPrimaryPermalinkEntity} from "../utils/permalinks/Permalinks";
function parseAtRoomMentions(text, partCreator) {
const ATROOM = "@room";
@@ -41,9 +39,8 @@ function parseAtRoomMentions(text, partCreator) {
function parseLink(a, partCreator) {
const {href} = a;
- const pillMatch = REGEX_MATRIXTO.exec(href) || [];
- const resourceId = pillMatch[1]; // The room/user ID
- const prefix = pillMatch[2]; // The first character of prefix
+ const resourceId = getPrimaryPermalinkEntity(href); // The room/user ID
+ const prefix = resourceId ? resourceId[0] : undefined; // First character of ID
switch (prefix) {
case "@":
return partCreator.userPill(a.textContent, resourceId);
diff --git a/src/editor/serialize.js b/src/editor/serialize.js
index 07a1ad908e..a55eed97da 100644
--- a/src/editor/serialize.js
+++ b/src/editor/serialize.js
@@ -16,6 +16,7 @@ limitations under the License.
*/
import Markdown from '../Markdown';
+import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
export function mdSerialize(model) {
return model.parts.reduce((html, part) => {
@@ -29,7 +30,7 @@ export function mdSerialize(model) {
return html + part.text;
case "room-pill":
case "user-pill":
- return html + `[${part.text}](https://matrix.to/#/${part.resourceId})`;
+ return html + `[${part.text}](${makeGenericPermalink(part.resourceId)})`;
}
}, "");
}
diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js
index a9e894d582..fabd9d15ad 100644
--- a/src/linkify-matrix.js
+++ b/src/linkify-matrix.js
@@ -1,5 +1,6 @@
/*
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.
@@ -14,7 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {baseUrl} from "./matrix-to";
+import {baseUrl} from "./utils/permalinks/SpecPermalinkConstructor";
+import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
function matrixLinkify(linkify) {
// Text tokens
@@ -189,13 +191,6 @@ matrixLinkify.MATRIXTO_MD_LINK_PATTERN =
'\\[([^\\]]*)\\]\\((?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!+][^\\)]*)\\)';
matrixLinkify.MATRIXTO_BASE_URL= baseUrl;
-const matrixToEntityMap = {
- '@': '#/user/',
- '#': '#/room/',
- '!': '#/room/',
- '+': '#/group/',
-};
-
matrixLinkify.options = {
events: function(href, type) {
switch (type) {
@@ -225,20 +220,8 @@ matrixLinkify.options = {
case 'roomalias':
case 'userid':
case 'groupid':
- return matrixLinkify.MATRIXTO_BASE_URL + '/#/' + href;
default: {
- // FIXME: horrible duplication with HtmlUtils' transform tags
- let m = href.match(matrixLinkify.VECTOR_URL_PATTERN);
- if (m) {
- return m[1];
- }
- m = href.match(matrixLinkify.MATRIXTO_URL_PATTERN);
- if (m) {
- const entity = m[1];
- if (matrixToEntityMap[entity[0]]) return matrixToEntityMap[entity[0]] + entity;
- }
-
- return href;
+ return tryTransformPermalinkToLocalHref(href);
}
}
},
@@ -249,8 +232,8 @@ matrixLinkify.options = {
target: function(href, type) {
if (type === 'url') {
- if (href.match(matrixLinkify.VECTOR_URL_PATTERN) ||
- href.match(matrixLinkify.MATRIXTO_URL_PATTERN)) {
+ const transformed = tryTransformPermalinkToLocalHref(href);
+ if (transformed !== href || href.match(matrixLinkify.VECTOR_URL_PATTERN)) {
return null;
} else {
return '_blank';
diff --git a/src/utils/permalinks/PermalinkConstructor.js b/src/utils/permalinks/PermalinkConstructor.js
new file mode 100644
index 0000000000..f74c432bf0
--- /dev/null
+++ b/src/utils/permalinks/PermalinkConstructor.js
@@ -0,0 +1,83 @@
+/*
+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.
+*/
+
+/**
+ * Interface for classes that actually produce permalinks (strings).
+ * TODO: Convert this to a real TypeScript interface
+ */
+export default class PermalinkConstructor {
+ forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
+ throw new Error("Not implemented");
+ }
+
+ forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
+ throw new Error("Not implemented");
+ }
+
+ forGroup(groupId: string): string {
+ throw new Error("Not implemented");
+ }
+
+ forUser(userId: string): string {
+ throw new Error("Not implemented");
+ }
+
+ forEntity(entityId: string): string {
+ throw new Error("Not implemented");
+ }
+
+ isPermalinkHost(host: string): boolean {
+ throw new Error("Not implemented");
+ }
+
+ parsePermalink(fullUrl: string): PermalinkParts {
+ throw new Error("Not implemented");
+ }
+}
+
+// Inspired by/Borrowed with permission from the matrix-bot-sdk:
+// https://github.com/turt2live/matrix-js-bot-sdk/blob/7c4665c9a25c2c8e0fe4e509f2616505b5b66a1c/src/Permalinks.ts#L1-L6
+export class PermalinkParts {
+ roomIdOrAlias: string;
+ eventId: string;
+ userId: string;
+ groupId: string;
+ viaServers: string[];
+
+ constructor(roomIdOrAlias: string, eventId: string, userId: string, groupId: string, viaServers: string[]) {
+ this.roomIdOrAlias = roomIdOrAlias;
+ this.eventId = eventId;
+ this.groupId = groupId;
+ this.userId = userId;
+ this.viaServers = viaServers;
+ }
+
+ static forUser(userId: string): PermalinkParts {
+ return new PermalinkParts(null, null, userId, null, null);
+ }
+
+ static forGroup(groupId: string): PermalinkParts {
+ return new PermalinkParts(null, null, null, groupId, null);
+ }
+
+ static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts {
+ return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []);
+ }
+
+ static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts {
+ return new PermalinkParts(roomId, eventId, null, null, viaServers || []);
+ }
+}
diff --git a/src/matrix-to.js b/src/utils/permalinks/Permalinks.js
similarity index 69%
rename from src/matrix-to.js
rename to src/utils/permalinks/Permalinks.js
index 14467cb4c5..19dcbd062a 100644
--- a/src/matrix-to.js
+++ b/src/utils/permalinks/Permalinks.js
@@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import MatrixClientPeg from "./MatrixClientPeg";
+import MatrixClientPeg from "../../MatrixClientPeg";
import isIp from "is-ip";
import utils from 'matrix-js-sdk/lib/utils';
+import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor";
+import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
+import RiotPermalinkConstructor from "./RiotPermalinkConstructor";
+import * as matrixLinkify from "../../linkify-matrix";
-export const host = "matrix.to";
-export const baseUrl = `https://${host}`;
+const SdkConfig = require("../../SdkConfig");
// The maximum number of servers to pick when working out which servers
// to add to permalinks. The servers are appended as ?via=example.org
@@ -73,7 +76,7 @@ export class RoomPermalinkCreator {
// We support being given a roomId as a fallback in the event the `room` object
// doesn't exist or is not healthy for us to rely on. For example, loading a
// permalink to a room which the MatrixClient doesn't know about.
- constructor(room, roomId=null) {
+ constructor(room, roomId = null) {
this._room = room;
this._roomId = room ? room.roomId : roomId;
this._highestPlUserId = null;
@@ -124,15 +127,11 @@ export class RoomPermalinkCreator {
}
forEvent(eventId) {
- const roomId = this._roomId;
- const permalinkBase = `${baseUrl}/#/${roomId}/${eventId}`;
- return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`;
+ return getPermalinkConstructor().forEvent(this._roomId, eventId, this._serverCandidates);
}
forRoom() {
- const roomId = this._roomId;
- const permalinkBase = `${baseUrl}/#/${roomId}`;
- return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`;
+ return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
}
onRoomState(event) {
@@ -182,8 +181,8 @@ export class RoomPermalinkCreator {
}
const serverName = getServerName(userId);
return !isHostnameIpAddress(serverName) &&
- !isHostInRegex(serverName, this._bannedHostsRegexps) &&
- isHostInRegex(serverName, this._allowedHostsRegexps);
+ !isHostInRegex(serverName, this._bannedHostsRegexps) &&
+ isHostInRegex(serverName, this._allowedHostsRegexps);
});
const maxEntry = allowedEntries.reduce((max, entry) => {
return (entry[1] > max[1]) ? entry : max;
@@ -221,7 +220,7 @@ export class RoomPermalinkCreator {
}
_updatePopulationMap() {
- const populationMap: {[server:string]:number} = {};
+ const populationMap: { [server: string]: number } = {};
for (const member of this._room.getJoinedMembers()) {
const serverName = getServerName(member.userId);
if (!populationMap[serverName]) {
@@ -242,9 +241,9 @@ export class RoomPermalinkCreator {
.sort((a, b) => this._populationMap[b] - this._populationMap[a])
.filter(a => {
return !candidates.includes(a) &&
- !isHostnameIpAddress(a) &&
- !isHostInRegex(a, this._bannedHostsRegexps) &&
- isHostInRegex(a, this._allowedHostsRegexps);
+ !isHostnameIpAddress(a) &&
+ !isHostInRegex(a, this._bannedHostsRegexps) &&
+ isHostInRegex(a, this._allowedHostsRegexps);
});
const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length);
@@ -254,25 +253,27 @@ export class RoomPermalinkCreator {
}
}
+export function makeGenericPermalink(entityId: string): string {
+ return getPermalinkConstructor().forEntity(entityId);
+}
+
export function makeUserPermalink(userId) {
- return `${baseUrl}/#/${userId}`;
+ return getPermalinkConstructor().forUser(userId);
}
export function makeRoomPermalink(roomId) {
- const permalinkBase = `${baseUrl}/#/${roomId}`;
-
if (!roomId) {
throw new Error("can't permalink a falsey roomId");
}
// If the roomId isn't actually a room ID, don't try to list the servers.
// Aliases are already routable, and don't need extra information.
- if (roomId[0] !== '!') return permalinkBase;
+ if (roomId[0] !== '!') return getPermalinkConstructor().forRoom(roomId, []);
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
if (!room) {
- return permalinkBase;
+ return getPermalinkConstructor().forRoom(roomId, []);
}
const permalinkCreator = new RoomPermalinkCreator(room);
permalinkCreator.load();
@@ -280,12 +281,96 @@ export function makeRoomPermalink(roomId) {
}
export function makeGroupPermalink(groupId) {
- return `${baseUrl}/#/${groupId}`;
+ return getPermalinkConstructor().forGroup(groupId);
}
-export function encodeServerCandidates(candidates) {
- if (!candidates || candidates.length === 0) return '';
- return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
+export function isPermalinkHost(host: string): boolean {
+ // Always check if the permalink is a spec permalink (callers are likely to call
+ // parsePermalink after this function).
+ if (new SpecPermalinkConstructor().isPermalinkHost(host)) return true;
+ return getPermalinkConstructor().isPermalinkHost(host);
+}
+
+/**
+ * Transforms a permalink (or possible permalink) into a local URL if possible. If
+ * the given permalink is found to not be a permalink, it'll be returned unaltered.
+ * @param {string} permalink The permalink to try and transform.
+ * @returns {string} The transformed permalink or original URL if unable.
+ */
+export function tryTransformPermalinkToLocalHref(permalink: string): string {
+ if (!permalink.startsWith("http:") && !permalink.startsWith("https:")) {
+ return permalink;
+ }
+
+ const m = permalink.match(matrixLinkify.VECTOR_URL_PATTERN);
+ if (m) {
+ return m[1];
+ }
+
+ // A bit of a hack to convert permalinks of unknown origin to Riot links
+ try {
+ const permalinkParts = parsePermalink(permalink);
+ if (permalinkParts) {
+ if (permalinkParts.roomIdOrAlias) {
+ const eventIdPart = permalinkParts.eventId ? `/${permalinkParts.eventId}` : '';
+ permalink = `#/room/${permalinkParts.roomIdOrAlias}${eventIdPart}`;
+ } else if (permalinkParts.groupId) {
+ permalink = `#/group/${permalinkParts.groupId}`;
+ } else if (permalinkParts.userId) {
+ permalink = `#/user/${permalinkParts.userId}`;
+ } // else not a valid permalink for our purposes - do not handle
+ }
+ } catch (e) {
+ // Not an href we need to care about
+ }
+
+ return permalink;
+}
+
+export function getPrimaryPermalinkEntity(permalink: string): string {
+ try {
+ let permalinkParts = parsePermalink(permalink);
+
+ // If not a permalink, try the vector patterns.
+ if (!permalinkParts) {
+ const m = permalink.match(matrixLinkify.VECTOR_URL_PATTERN);
+ if (m) {
+ // A bit of a hack, but it gets the job done
+ const handler = new RiotPermalinkConstructor("http://localhost");
+ const entityInfo = m[1].split('#').slice(1).join('#');
+ permalinkParts = handler.parsePermalink(`http://localhost/#${entityInfo}`);
+ }
+ }
+
+ if (!permalinkParts) return null; // not processable
+ if (permalinkParts.userId) return permalinkParts.userId;
+ if (permalinkParts.groupId) return permalinkParts.groupId;
+ if (permalinkParts.roomIdOrAlias) return permalinkParts.roomIdOrAlias;
+ } catch (e) {
+ // no entity - not a permalink
+ }
+
+ return null;
+}
+
+function getPermalinkConstructor(): PermalinkConstructor {
+ const riotPrefix = SdkConfig.get()['permalinkPrefix'];
+ if (riotPrefix && riotPrefix !== matrixtoBaseUrl) {
+ return new RiotPermalinkConstructor(riotPrefix);
+ }
+
+ return new SpecPermalinkConstructor();
+}
+
+export function parsePermalink(fullUrl: string): PermalinkParts {
+ const riotPrefix = SdkConfig.get()['permalinkPrefix'];
+ if (fullUrl.startsWith(matrixtoBaseUrl)) {
+ return new SpecPermalinkConstructor().parsePermalink(fullUrl);
+ } else if (riotPrefix && fullUrl.startsWith(riotPrefix)) {
+ return new RiotPermalinkConstructor(riotPrefix).parsePermalink(fullUrl);
+ }
+
+ return null; // not a permalink we can handle
}
function getServerName(userId) {
diff --git a/src/utils/permalinks/RiotPermalinkConstructor.js b/src/utils/permalinks/RiotPermalinkConstructor.js
new file mode 100644
index 0000000000..176100aa8b
--- /dev/null
+++ b/src/utils/permalinks/RiotPermalinkConstructor.js
@@ -0,0 +1,111 @@
+/*
+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 PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
+
+/**
+ * Generates permalinks that self-reference the running webapp
+ */
+export default class RiotPermalinkConstructor extends PermalinkConstructor {
+ _riotUrl: string;
+
+ constructor(riotUrl: string) {
+ super();
+ this._riotUrl = riotUrl;
+
+ if (!this._riotUrl.startsWith("http:") && !this._riotUrl.startsWith("https:")) {
+ throw new Error("Riot prefix URL does not appear to be an HTTP(S) URL");
+ }
+ }
+
+ forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
+ return `${this._riotUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`;
+ }
+
+ forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
+ return `${this._riotUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`;
+ }
+
+ forUser(userId: string): string {
+ return `${this._riotUrl}/#/user/${userId}`;
+ }
+
+ forGroup(groupId: string): string {
+ return `${this._riotUrl}/#/group/${groupId}`;
+ }
+
+ forEntity(entityId: string): string {
+ if (entityId[0] === '!' || entityId[0] === '#') {
+ return this.forRoom(entityId);
+ } else if (entityId[0] === '@') {
+ return this.forUser(entityId);
+ } else if (entityId[0] === '+') {
+ return this.forGroup(entityId);
+ } else throw new Error("Unrecognized entity");
+ }
+
+ isPermalinkHost(testHost: string): boolean {
+ const parsedUrl = new URL(this._riotUrl);
+ return testHost === (parsedUrl.host || parsedUrl.hostname); // one of the hosts should match
+ }
+
+ encodeServerCandidates(candidates: string[]) {
+ if (!candidates || candidates.length === 0) return '';
+ return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
+ }
+
+ // Heavily inspired by/borrowed from the matrix-bot-sdk (with permission):
+ // https://github.com/turt2live/matrix-js-bot-sdk/blob/7c4665c9a25c2c8e0fe4e509f2616505b5b66a1c/src/Permalinks.ts#L33-L61
+ // Adapted for Riot's URL format
+ parsePermalink(fullUrl: string): PermalinkParts {
+ if (!fullUrl || !fullUrl.startsWith(this._riotUrl)) {
+ throw new Error("Does not appear to be a permalink");
+ }
+
+ const parts = fullUrl.substring(`${this._riotUrl}/#/`.length).split("/");
+ if (parts.length < 2) { // we're expecting an entity and an ID of some kind at least
+ throw new Error("URL is missing parts");
+ }
+
+ const entityType = parts[0];
+ const entity = parts[1];
+ if (entityType === 'user') {
+ // Probably a user, no further parsing needed.
+ return PermalinkParts.forUser(entity);
+ } else if (entityType === 'group') {
+ // Probably a group, no further parsing needed.
+ return PermalinkParts.forGroup(entity);
+ } else if (entityType === 'room') {
+ if (parts.length === 2) {
+ return PermalinkParts.forRoom(entity, []);
+ }
+
+ // rejoin the rest because v3 events can have slashes (annoyingly)
+ const eventIdAndQuery = parts.length > 2 ? parts.slice(2).join('/') : "";
+ const secondaryParts = eventIdAndQuery.split("?");
+
+ const eventId = secondaryParts[0];
+ const query = secondaryParts.length > 1 ? secondaryParts[1] : "";
+
+ // TODO: Verify Riot works with via args
+ const via = query.split("via=").filter(p => !!p);
+
+ return PermalinkParts.forEvent(entity, eventId, via);
+ } else {
+ throw new Error("Unknown entity type in permalink");
+ }
+ }
+}
diff --git a/src/utils/permalinks/SpecPermalinkConstructor.js b/src/utils/permalinks/SpecPermalinkConstructor.js
new file mode 100644
index 0000000000..1c80ff8975
--- /dev/null
+++ b/src/utils/permalinks/SpecPermalinkConstructor.js
@@ -0,0 +1,94 @@
+/*
+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 PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
+
+export const host = "matrix.to";
+export const baseUrl = `https://${host}`;
+
+/**
+ * Generates matrix.to permalinks
+ */
+export default class SpecPermalinkConstructor extends PermalinkConstructor {
+ constructor() {
+ super();
+ }
+
+ forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
+ return `${baseUrl}/#/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`;
+ }
+
+ forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
+ return `${baseUrl}/#/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`;
+ }
+
+ forUser(userId: string): string {
+ return `${baseUrl}/#/${userId}`;
+ }
+
+ forGroup(groupId: string): string {
+ return `${baseUrl}/#/${groupId}`;
+ }
+
+ forEntity(entityId: string): string {
+ return `${baseUrl}/#/${entityId}`;
+ }
+
+ isPermalinkHost(testHost: string): boolean {
+ return testHost === host;
+ }
+
+ encodeServerCandidates(candidates: string[]) {
+ if (!candidates || candidates.length === 0) return '';
+ return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
+ }
+
+ // Heavily inspired by/borrowed from the matrix-bot-sdk (with permission):
+ // https://github.com/turt2live/matrix-js-bot-sdk/blob/7c4665c9a25c2c8e0fe4e509f2616505b5b66a1c/src/Permalinks.ts#L33-L61
+ parsePermalink(fullUrl: string): PermalinkParts {
+ if (!fullUrl || !fullUrl.startsWith(baseUrl)) {
+ throw new Error("Does not appear to be a permalink");
+ }
+
+ const parts = fullUrl.substring(`${baseUrl}/#/`.length).split("/");
+
+ const entity = parts[0];
+ if (entity[0] === '@') {
+ // Probably a user, no further parsing needed.
+ return PermalinkParts.forUser(entity);
+ } else if (entity[0] === '+') {
+ // Probably a group, no further parsing needed.
+ return PermalinkParts.forGroup(entity);
+ } else if (entity[0] === '#' || entity[0] === '!') {
+ if (parts.length === 1) {
+ return PermalinkParts.forRoom(entity, []);
+ }
+
+ // rejoin the rest because v3 events can have slashes (annoyingly)
+ const eventIdAndQuery = parts.length > 1 ? parts.slice(1).join('/') : "";
+ const secondaryParts = eventIdAndQuery.split("?");
+
+ const eventId = secondaryParts[0];
+ const query = secondaryParts.length > 1 ? secondaryParts[1] : "";
+
+ const via = query.split("via=").filter(p => !!p);
+
+ return PermalinkParts.forEvent(entity, eventId, via);
+ } else {
+ throw new Error("Unknown entity type in permalink");
+ }
+ }
+}
diff --git a/test/matrix-to-test.js b/test/utils/permalinks/Permalinks-test.js
similarity index 98%
rename from test/matrix-to-test.js
rename to test/utils/permalinks/Permalinks-test.js
index 23434a57e2..27f06b44cb 100644
--- a/test/matrix-to-test.js
+++ b/test/utils/permalinks/Permalinks-test.js
@@ -1,5 +1,7 @@
/*
Copyright 2018 New Vector 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
@@ -12,14 +14,14 @@ limitations under the License.
*/
import expect from 'expect';
-import peg from '../src/MatrixClientPeg';
+import peg from '../../../src/MatrixClientPeg';
import {
makeGroupPermalink,
makeRoomPermalink,
makeUserPermalink,
RoomPermalinkCreator,
-} from "../src/matrix-to";
-import * as testUtils from "./test-utils";
+} from "../../../src/utils/permalinks/Permalinks";
+import * as testUtils from "../../test-utils";
function mockRoom(roomId, members, serverACL) {
members.forEach(m => m.membership = "join");
@@ -62,7 +64,7 @@ function mockRoom(roomId, members, serverACL) {
};
}
-describe('matrix-to', function() {
+describe('Permalinks', function() {
let sandbox;
beforeEach(function() {