mirror of
				https://github.com/vector-im/element-web.git
				synced 2025-11-04 02:02:14 +01:00 
			
		
		
		
	Merge pull request #3500 from matrix-org/travis/permalinks
Support local permalinks for unfederated instances
This commit is contained in:
		
						commit
						7d1a04cb12
					
				@ -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:"))
 | 
			
		||||
                    );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
@ -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 = [
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
            if (nextProps.inMessage) {
 | 
			
		||||
                // Default to the empty array if no match for simplicity
 | 
			
		||||
                // resource and prefix will be undefined instead of throwing
 | 
			
		||||
            matrixToMatch = regex.exec(nextProps.url) || [];
 | 
			
		||||
                const matrixToMatch = REGEX_LOCAL_PERMALINK.exec(nextProps.url) || [];
 | 
			
		||||
 | 
			
		||||
                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 || {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
@ -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());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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({
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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)})`;
 | 
			
		||||
        }
 | 
			
		||||
    }, "");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@ -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) {
 | 
			
		||||
@ -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) {
 | 
			
		||||
							
								
								
									
										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 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() {
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user