Merge pull request #3500 from matrix-org/travis/permalinks
Support local permalinks for unfederated instances
This commit is contained in:
commit
7d1a04cb12
24 changed files with 486 additions and 146 deletions
|
@ -2,6 +2,7 @@
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 from 'emojibase-data/en/compact.json';
|
||||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||||
|
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -158,30 +160,10 @@ const transformTags = { // custom to matrix
|
||||||
if (attribs.href) {
|
if (attribs.href) {
|
||||||
attribs.target = '_blank'; // by default
|
attribs.target = '_blank'; // by default
|
||||||
|
|
||||||
let m;
|
const transformed = tryTransformPermalinkToLocalHref(attribs.href);
|
||||||
// FIXME: horrible duplication with linkify-matrix
|
if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN)) {
|
||||||
m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
|
attribs.href = transformed;
|
||||||
if (m) {
|
|
||||||
attribs.href = m[1];
|
|
||||||
delete attribs.target;
|
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/
|
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);
|
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
|
||||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length &&
|
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length &&
|
||||||
// Prevent user pills expanding for users with only emoji in
|
// 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 == undefined ||
|
||||||
!content.formatted_body.includes("https://matrix.to/")
|
(!content.formatted_body.includes("http:") &&
|
||||||
|
!content.formatted_body.includes("https:"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,6 @@ import dis from './dispatcher';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
import {_t, _td} from './languageHandler';
|
import {_t, _td} from './languageHandler';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import {MATRIXTO_URL_PATTERN} from "./linkify-matrix";
|
|
||||||
import * as querystring from "querystring";
|
|
||||||
import MultiInviter from './utils/MultiInviter';
|
import MultiInviter from './utils/MultiInviter';
|
||||||
import { linkifyAndSanitizeHtml } from './HtmlUtils';
|
import { linkifyAndSanitizeHtml } from './HtmlUtils';
|
||||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||||
|
@ -34,6 +32,7 @@ import Promise from "bluebird";
|
||||||
import { getAddressType } from './UserAddress';
|
import { getAddressType } from './UserAddress';
|
||||||
import { abbreviateUrl } from './utils/UrlUtils';
|
import { abbreviateUrl } from './utils/UrlUtils';
|
||||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
|
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
|
||||||
|
import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks";
|
||||||
|
|
||||||
const singleMxcUpload = async () => {
|
const singleMxcUpload = async () => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -441,7 +440,19 @@ export const CommandMap = {
|
||||||
const params = args.split(' ');
|
const params = args.split(' ');
|
||||||
if (params.length < 1) return reject(this.getUsage());
|
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] === '#') {
|
if (params[0][0] === '#') {
|
||||||
let roomAlias = params[0];
|
let roomAlias = params[0];
|
||||||
if (!roomAlias.includes(':')) {
|
if (!roomAlias.includes(':')) {
|
||||||
|
@ -469,29 +480,25 @@ export const CommandMap = {
|
||||||
auto_join: true,
|
auto_join: true,
|
||||||
});
|
});
|
||||||
return success();
|
return success();
|
||||||
} else if (matrixToMatches) {
|
} else if (isPermalink) {
|
||||||
let entity = matrixToMatches[1];
|
const permalinkParts = parsePermalink(params[0]);
|
||||||
let eventId = null;
|
|
||||||
let viaServers = [];
|
|
||||||
|
|
||||||
if (entity[0] !== '!' && entity[0] !== '#') return reject(this.getUsage());
|
// This check technically isn't needed because we already did our
|
||||||
|
// safety checks up above. However, for good measure, let's be sure.
|
||||||
if (entity.indexOf('?') !== -1) {
|
if (!permalinkParts) {
|
||||||
const parts = entity.split('?');
|
return reject(this.getUsage());
|
||||||
entity = parts[0];
|
|
||||||
|
|
||||||
const parsed = querystring.parse(parts[1]);
|
|
||||||
viaServers = parsed["via"];
|
|
||||||
if (typeof viaServers === 'string') viaServers = [viaServers];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We quietly support event ID permalinks too
|
// If for some reason someone wanted to join a group or user, we should
|
||||||
if (entity.indexOf('/$') !== -1) {
|
// stop them now.
|
||||||
const parts = entity.split("/$");
|
if (!permalinkParts.roomIdOrAlias) {
|
||||||
entity = parts[0];
|
return reject(this.getUsage());
|
||||||
eventId = `$${parts[1]}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const entity = permalinkParts.roomIdOrAlias;
|
||||||
|
const viaServers = permalinkParts.viaServers;
|
||||||
|
const eventId = permalinkParts.eventId;
|
||||||
|
|
||||||
const dispatch = {
|
const dispatch = {
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
auto_join: true,
|
auto_join: true,
|
||||||
|
|
|
@ -23,7 +23,7 @@ import QueryMatcher from './QueryMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import {makeGroupPermalink} from "../matrix-to";
|
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
|
||||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||||
import FlairStore from "../stores/FlairStore";
|
import FlairStore from "../stores/FlairStore";
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {PillCompletion} from './Components';
|
||||||
import {getDisplayAliasForRoom} from '../Rooms';
|
import {getDisplayAliasForRoom} from '../Rooms';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import {makeRoomPermalink} from "../matrix-to";
|
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
||||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const ROOM_REGEX = /\B#\S*/g;
|
const ROOM_REGEX = /\B#\S*/g;
|
||||||
|
|
|
@ -28,7 +28,7 @@ import _sortBy from 'lodash/sortBy';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
|
|
||||||
import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk';
|
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";
|
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
const USER_REGEX = /\B@\S*/g;
|
const USER_REGEX = /\B@\S*/g;
|
||||||
|
|
|
@ -36,7 +36,7 @@ import classnames from 'classnames';
|
||||||
import GroupStore from '../../stores/GroupStore';
|
import GroupStore from '../../stores/GroupStore';
|
||||||
import FlairStore from '../../stores/FlairStore';
|
import FlairStore from '../../stores/FlairStore';
|
||||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||||
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
|
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
||||||
import {Group} from "matrix-js-sdk";
|
import {Group} from "matrix-js-sdk";
|
||||||
|
|
||||||
const LONG_DESC_PLACEHOLDER = _td(
|
const LONG_DESC_PLACEHOLDER = _td(
|
||||||
|
|
|
@ -31,7 +31,7 @@ import Promise from 'bluebird';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {Room} from "matrix-js-sdk";
|
import {Room} from "matrix-js-sdk";
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import {RoomPermalinkCreator} from '../../matrix-to';
|
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import ContentMessages from '../../ContentMessages';
|
import ContentMessages from '../../ContentMessages';
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import QRCode from 'qrcode-react';
|
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";
|
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||||
|
|
||||||
const socials = [
|
const socials = [
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2018 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
|
||||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||||
import FlairStore from "../../../stores/FlairStore";
|
import FlairStore from "../../../stores/FlairStore";
|
||||||
|
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
|
||||||
|
|
||||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
// 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({
|
const Pill = createReactClass({
|
||||||
statics: {
|
statics: {
|
||||||
isPillUrl: (url) => {
|
isPillUrl: (url) => {
|
||||||
return !!REGEX_MATRIXTO.exec(url);
|
return !!getPrimaryPermalinkEntity(url);
|
||||||
},
|
},
|
||||||
isMessagePillUrl: (url) => {
|
isMessagePillUrl: (url) => {
|
||||||
return !!REGEX_LOCAL_MATRIXTO.exec(url);
|
return !!REGEX_LOCAL_PERMALINK.exec(url);
|
||||||
},
|
},
|
||||||
roomNotifPos: (text) => {
|
roomNotifPos: (text) => {
|
||||||
return text.indexOf("@room");
|
return text.indexOf("@room");
|
||||||
|
@ -95,22 +94,21 @@ const Pill = createReactClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
async componentWillReceiveProps(nextProps) {
|
async componentWillReceiveProps(nextProps) {
|
||||||
let regex = REGEX_MATRIXTO;
|
|
||||||
if (nextProps.inMessage) {
|
|
||||||
regex = REGEX_LOCAL_MATRIXTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
let matrixToMatch;
|
|
||||||
let resourceId;
|
let resourceId;
|
||||||
let prefix;
|
let prefix;
|
||||||
|
|
||||||
if (nextProps.url) {
|
if (nextProps.url) {
|
||||||
// Default to the empty array if no match for simplicity
|
if (nextProps.inMessage) {
|
||||||
// resource and prefix will be undefined instead of throwing
|
// Default to the empty array if no match for simplicity
|
||||||
matrixToMatch = regex.exec(nextProps.url) || [];
|
// resource and prefix will be undefined instead of throwing
|
||||||
|
const matrixToMatch = REGEX_LOCAL_PERMALINK.exec(nextProps.url) || [];
|
||||||
|
|
||||||
resourceId = matrixToMatch[1]; // The room/user ID
|
resourceId = matrixToMatch[1]; // The room/user ID
|
||||||
prefix = matrixToMatch[2]; // The first character of prefix
|
prefix = matrixToMatch[2]; // The first character of prefix
|
||||||
|
} else {
|
||||||
|
resourceId = getPrimaryPermalinkEntity(nextProps.url);
|
||||||
|
prefix = resourceId ? resourceId[0] : undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pillType = this.props.type || {
|
const pillType = this.props.type || {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import {wantsDateSeparator} from '../../../DateUtils';
|
import {wantsDateSeparator} from '../../../DateUtils';
|
||||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
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";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
// This component does no cycle detection, simply because the only way to make such a cycle would be to
|
||||||
|
|
|
@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
|
||||||
import createReactClass from 'create-react-class';
|
import createReactClass from 'create-react-class';
|
||||||
|
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { RoomPermalinkCreator } from '../../../matrix-to';
|
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
|
|
|
@ -30,9 +30,9 @@ import { _t } from '../../../languageHandler';
|
||||||
import * as ContextualMenu from '../../structures/ContextualMenu';
|
import * as ContextualMenu from '../../structures/ContextualMenu';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import {host as matrixtoHost} from '../../../matrix-to';
|
|
||||||
import {pillifyLinks} from '../../../utils/pillify';
|
import {pillifyLinks} from '../../../utils/pillify';
|
||||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||||
|
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
||||||
|
|
||||||
module.exports = createReactClass({
|
module.exports = createReactClass({
|
||||||
displayName: 'TextualBody',
|
displayName: 'TextualBody',
|
||||||
|
@ -248,10 +248,10 @@ module.exports = createReactClass({
|
||||||
const url = node.getAttribute("href");
|
const url = node.getAttribute("href");
|
||||||
const host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1];
|
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
|
// preview of the room/user they point to: nobody needs to be reminded
|
||||||
// what the matrix.to site looks like).
|
// 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())) {
|
if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) {
|
||||||
// it's a "foo.pl" style link
|
// it's a "foo.pl" style link
|
||||||
|
|
|
@ -23,7 +23,7 @@ import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import Stickerpicker from './Stickerpicker';
|
import Stickerpicker from './Stickerpicker';
|
||||||
import { makeRoomPermalink } from '../../../matrix-to';
|
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
|
|
|
@ -48,13 +48,11 @@ import Markdown from '../../../Markdown';
|
||||||
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
|
|
||||||
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
|
|
||||||
|
|
||||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||||
|
|
||||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
import {makeUserPermalink} from "../../../matrix-to";
|
import {getPrimaryPermalinkEntity, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
|
@ -224,18 +222,15 @@ export default class MessageComposerInput extends React.Component {
|
||||||
// special case links
|
// special case links
|
||||||
if (tag === 'a') {
|
if (tag === 'a') {
|
||||||
const href = el.getAttribute('href');
|
const href = el.getAttribute('href');
|
||||||
let m;
|
const permalinkEntity = getPrimaryPermalinkEntity(href);
|
||||||
if (href) {
|
if (permalinkEntity) {
|
||||||
m = href.match(MATRIXTO_URL_PATTERN);
|
|
||||||
}
|
|
||||||
if (m) {
|
|
||||||
return {
|
return {
|
||||||
object: 'inline',
|
object: 'inline',
|
||||||
type: 'pill',
|
type: 'pill',
|
||||||
data: {
|
data: {
|
||||||
href,
|
href,
|
||||||
completion: el.innerText,
|
completion: el.innerText,
|
||||||
completionId: m[1],
|
completionId: permalinkEntity,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
@ -541,7 +536,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
const textWithMdPills = this.plainWithMdPills.serialize(editorState);
|
const textWithMdPills = this.plainWithMdPills.serialize(editorState);
|
||||||
const markdown = new Markdown(textWithMdPills);
|
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());
|
return this.html.deserialize(markdown.toHTML());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import {RoomPermalinkCreator} from "../../../matrix-to";
|
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||||
|
|
||||||
function cancelQuoting() {
|
function cancelQuoting() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
|
|
@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
import Stickerpicker from './Stickerpicker';
|
import Stickerpicker from './Stickerpicker';
|
||||||
import { makeRoomPermalink } from '../../../matrix-to';
|
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
|
|
||||||
import { walkDOMDepthFirst } from "./dom";
|
import { walkDOMDepthFirst } from "./dom";
|
||||||
import { checkBlockNode } from "../HtmlUtils";
|
import { checkBlockNode } from "../HtmlUtils";
|
||||||
|
import {getPrimaryPermalinkEntity} from "../utils/permalinks/Permalinks";
|
||||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
|
||||||
|
|
||||||
function parseAtRoomMentions(text, partCreator) {
|
function parseAtRoomMentions(text, partCreator) {
|
||||||
const ATROOM = "@room";
|
const ATROOM = "@room";
|
||||||
|
@ -41,9 +39,8 @@ function parseAtRoomMentions(text, partCreator) {
|
||||||
|
|
||||||
function parseLink(a, partCreator) {
|
function parseLink(a, partCreator) {
|
||||||
const {href} = a;
|
const {href} = a;
|
||||||
const pillMatch = REGEX_MATRIXTO.exec(href) || [];
|
const resourceId = getPrimaryPermalinkEntity(href); // The room/user ID
|
||||||
const resourceId = pillMatch[1]; // The room/user ID
|
const prefix = resourceId ? resourceId[0] : undefined; // First character of ID
|
||||||
const prefix = pillMatch[2]; // The first character of prefix
|
|
||||||
switch (prefix) {
|
switch (prefix) {
|
||||||
case "@":
|
case "@":
|
||||||
return partCreator.userPill(a.textContent, resourceId);
|
return partCreator.userPill(a.textContent, resourceId);
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Markdown from '../Markdown';
|
import Markdown from '../Markdown';
|
||||||
|
import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
|
||||||
|
|
||||||
export function mdSerialize(model) {
|
export function mdSerialize(model) {
|
||||||
return model.parts.reduce((html, part) => {
|
return model.parts.reduce((html, part) => {
|
||||||
|
@ -29,7 +30,7 @@ export function mdSerialize(model) {
|
||||||
return html + part.text;
|
return html + part.text;
|
||||||
case "room-pill":
|
case "room-pill":
|
||||||
case "user-pill":
|
case "user-pill":
|
||||||
return html + `[${part.text}](https://matrix.to/#/${part.resourceId})`;
|
return html + `[${part.text}](${makeGenericPermalink(part.resourceId)})`;
|
||||||
}
|
}
|
||||||
}, "");
|
}, "");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {baseUrl} from "./matrix-to";
|
import {baseUrl} from "./utils/permalinks/SpecPermalinkConstructor";
|
||||||
|
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||||
|
|
||||||
function matrixLinkify(linkify) {
|
function matrixLinkify(linkify) {
|
||||||
// Text tokens
|
// Text tokens
|
||||||
|
@ -189,13 +191,6 @@ matrixLinkify.MATRIXTO_MD_LINK_PATTERN =
|
||||||
'\\[([^\\]]*)\\]\\((?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!+][^\\)]*)\\)';
|
'\\[([^\\]]*)\\]\\((?:https?://)?(?:www\\.)?matrix\\.to/#/([#@!+][^\\)]*)\\)';
|
||||||
matrixLinkify.MATRIXTO_BASE_URL= baseUrl;
|
matrixLinkify.MATRIXTO_BASE_URL= baseUrl;
|
||||||
|
|
||||||
const matrixToEntityMap = {
|
|
||||||
'@': '#/user/',
|
|
||||||
'#': '#/room/',
|
|
||||||
'!': '#/room/',
|
|
||||||
'+': '#/group/',
|
|
||||||
};
|
|
||||||
|
|
||||||
matrixLinkify.options = {
|
matrixLinkify.options = {
|
||||||
events: function(href, type) {
|
events: function(href, type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -225,20 +220,8 @@ matrixLinkify.options = {
|
||||||
case 'roomalias':
|
case 'roomalias':
|
||||||
case 'userid':
|
case 'userid':
|
||||||
case 'groupid':
|
case 'groupid':
|
||||||
return matrixLinkify.MATRIXTO_BASE_URL + '/#/' + href;
|
|
||||||
default: {
|
default: {
|
||||||
// FIXME: horrible duplication with HtmlUtils' transform tags
|
return tryTransformPermalinkToLocalHref(href);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -249,8 +232,8 @@ matrixLinkify.options = {
|
||||||
|
|
||||||
target: function(href, type) {
|
target: function(href, type) {
|
||||||
if (type === 'url') {
|
if (type === 'url') {
|
||||||
if (href.match(matrixLinkify.VECTOR_URL_PATTERN) ||
|
const transformed = tryTransformPermalinkToLocalHref(href);
|
||||||
href.match(matrixLinkify.MATRIXTO_URL_PATTERN)) {
|
if (transformed !== href || href.match(matrixLinkify.VECTOR_URL_PATTERN)) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return '_blank';
|
return '_blank';
|
||||||
|
|
83
src/utils/permalinks/PermalinkConstructor.js
Normal file
83
src/utils/permalinks/PermalinkConstructor.js
Normal file
|
@ -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 || []);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MatrixClientPeg from "./MatrixClientPeg";
|
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||||
import isIp from "is-ip";
|
import isIp from "is-ip";
|
||||||
import utils from 'matrix-js-sdk/lib/utils';
|
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";
|
const SdkConfig = require("../../SdkConfig");
|
||||||
export const baseUrl = `https://${host}`;
|
|
||||||
|
|
||||||
// The maximum number of servers to pick when working out which servers
|
// The maximum number of servers to pick when working out which servers
|
||||||
// to add to permalinks. The servers are appended as ?via=example.org
|
// 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
|
// 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
|
// 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.
|
// permalink to a room which the MatrixClient doesn't know about.
|
||||||
constructor(room, roomId=null) {
|
constructor(room, roomId = null) {
|
||||||
this._room = room;
|
this._room = room;
|
||||||
this._roomId = room ? room.roomId : roomId;
|
this._roomId = room ? room.roomId : roomId;
|
||||||
this._highestPlUserId = null;
|
this._highestPlUserId = null;
|
||||||
|
@ -124,15 +127,11 @@ export class RoomPermalinkCreator {
|
||||||
}
|
}
|
||||||
|
|
||||||
forEvent(eventId) {
|
forEvent(eventId) {
|
||||||
const roomId = this._roomId;
|
return getPermalinkConstructor().forEvent(this._roomId, eventId, this._serverCandidates);
|
||||||
const permalinkBase = `${baseUrl}/#/${roomId}/${eventId}`;
|
|
||||||
return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
forRoom() {
|
forRoom() {
|
||||||
const roomId = this._roomId;
|
return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates);
|
||||||
const permalinkBase = `${baseUrl}/#/${roomId}`;
|
|
||||||
return `${permalinkBase}${encodeServerCandidates(this._serverCandidates)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRoomState(event) {
|
onRoomState(event) {
|
||||||
|
@ -182,8 +181,8 @@ export class RoomPermalinkCreator {
|
||||||
}
|
}
|
||||||
const serverName = getServerName(userId);
|
const serverName = getServerName(userId);
|
||||||
return !isHostnameIpAddress(serverName) &&
|
return !isHostnameIpAddress(serverName) &&
|
||||||
!isHostInRegex(serverName, this._bannedHostsRegexps) &&
|
!isHostInRegex(serverName, this._bannedHostsRegexps) &&
|
||||||
isHostInRegex(serverName, this._allowedHostsRegexps);
|
isHostInRegex(serverName, this._allowedHostsRegexps);
|
||||||
});
|
});
|
||||||
const maxEntry = allowedEntries.reduce((max, entry) => {
|
const maxEntry = allowedEntries.reduce((max, entry) => {
|
||||||
return (entry[1] > max[1]) ? entry : max;
|
return (entry[1] > max[1]) ? entry : max;
|
||||||
|
@ -221,7 +220,7 @@ export class RoomPermalinkCreator {
|
||||||
}
|
}
|
||||||
|
|
||||||
_updatePopulationMap() {
|
_updatePopulationMap() {
|
||||||
const populationMap: {[server:string]:number} = {};
|
const populationMap: { [server: string]: number } = {};
|
||||||
for (const member of this._room.getJoinedMembers()) {
|
for (const member of this._room.getJoinedMembers()) {
|
||||||
const serverName = getServerName(member.userId);
|
const serverName = getServerName(member.userId);
|
||||||
if (!populationMap[serverName]) {
|
if (!populationMap[serverName]) {
|
||||||
|
@ -242,9 +241,9 @@ export class RoomPermalinkCreator {
|
||||||
.sort((a, b) => this._populationMap[b] - this._populationMap[a])
|
.sort((a, b) => this._populationMap[b] - this._populationMap[a])
|
||||||
.filter(a => {
|
.filter(a => {
|
||||||
return !candidates.includes(a) &&
|
return !candidates.includes(a) &&
|
||||||
!isHostnameIpAddress(a) &&
|
!isHostnameIpAddress(a) &&
|
||||||
!isHostInRegex(a, this._bannedHostsRegexps) &&
|
!isHostInRegex(a, this._bannedHostsRegexps) &&
|
||||||
isHostInRegex(a, this._allowedHostsRegexps);
|
isHostInRegex(a, this._allowedHostsRegexps);
|
||||||
});
|
});
|
||||||
|
|
||||||
const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length);
|
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) {
|
export function makeUserPermalink(userId) {
|
||||||
return `${baseUrl}/#/${userId}`;
|
return getPermalinkConstructor().forUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeRoomPermalink(roomId) {
|
export function makeRoomPermalink(roomId) {
|
||||||
const permalinkBase = `${baseUrl}/#/${roomId}`;
|
|
||||||
|
|
||||||
if (!roomId) {
|
if (!roomId) {
|
||||||
throw new Error("can't permalink a falsey 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.
|
// 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.
|
// 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 client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return permalinkBase;
|
return getPermalinkConstructor().forRoom(roomId, []);
|
||||||
}
|
}
|
||||||
const permalinkCreator = new RoomPermalinkCreator(room);
|
const permalinkCreator = new RoomPermalinkCreator(room);
|
||||||
permalinkCreator.load();
|
permalinkCreator.load();
|
||||||
|
@ -280,12 +281,96 @@ export function makeRoomPermalink(roomId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeGroupPermalink(groupId) {
|
export function makeGroupPermalink(groupId) {
|
||||||
return `${baseUrl}/#/${groupId}`;
|
return getPermalinkConstructor().forGroup(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encodeServerCandidates(candidates) {
|
export function isPermalinkHost(host: string): boolean {
|
||||||
if (!candidates || candidates.length === 0) return '';
|
// Always check if the permalink is a spec permalink (callers are likely to call
|
||||||
return `?via=${candidates.map(c => encodeURIComponent(c)).join("&via=")}`;
|
// 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) {
|
function getServerName(userId) {
|
111
src/utils/permalinks/RiotPermalinkConstructor.js
Normal file
111
src/utils/permalinks/RiotPermalinkConstructor.js
Normal file
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
src/utils/permalinks/SpecPermalinkConstructor.js
Normal file
94
src/utils/permalinks/SpecPermalinkConstructor.js
Normal file
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
@ -12,14 +14,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import expect from 'expect';
|
import expect from 'expect';
|
||||||
import peg from '../src/MatrixClientPeg';
|
import peg from '../../../src/MatrixClientPeg';
|
||||||
import {
|
import {
|
||||||
makeGroupPermalink,
|
makeGroupPermalink,
|
||||||
makeRoomPermalink,
|
makeRoomPermalink,
|
||||||
makeUserPermalink,
|
makeUserPermalink,
|
||||||
RoomPermalinkCreator,
|
RoomPermalinkCreator,
|
||||||
} from "../src/matrix-to";
|
} from "../../../src/utils/permalinks/Permalinks";
|
||||||
import * as testUtils from "./test-utils";
|
import * as testUtils from "../../test-utils";
|
||||||
|
|
||||||
function mockRoom(roomId, members, serverACL) {
|
function mockRoom(roomId, members, serverACL) {
|
||||||
members.forEach(m => m.membership = "join");
|
members.forEach(m => m.membership = "join");
|
||||||
|
@ -62,7 +64,7 @@ function mockRoom(roomId, members, serverACL) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('matrix-to', function() {
|
describe('Permalinks', function() {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
Loading…
Add table
Add a link
Reference in a new issue