1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00
Files
matrix-js-sdk/src/utils.ts
2021-06-09 21:54:17 -06:00

564 lines
19 KiB
TypeScript

/*
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.
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.
*/
/**
* This is an internal module.
* @module utils
*/
import unhomoglyph from 'unhomoglyph';
/**
* Encode a dictionary of query parameters.
* @param {Object} params A dict of key/values to encode e.g.
* {"foo": "bar", "baz": "taz"}
* @return {string} The encoded string e.g. foo=bar&baz=taz
*/
export function encodeParams(params: Record<string, string>): string {
let qs = "";
for (const key in params) {
if (!params.hasOwnProperty(key)) {
continue;
}
qs += "&" + encodeURIComponent(key) + "=" +
encodeURIComponent(params[key]);
}
return qs.substring(1);
}
/**
* Encodes a URI according to a set of template variables. Variables will be
* passed through encodeURIComponent.
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
* @param {Object} variables The key/value pairs to replace the template
* variables with. E.g. { "$bar": "baz" }.
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
export function encodeUri(pathTemplate: string,
variables: Record<string, string>): string {
for (const key in variables) {
if (!variables.hasOwnProperty(key)) {
continue;
}
pathTemplate = pathTemplate.replace(
key, encodeURIComponent(variables[key]),
);
}
return pathTemplate;
}
/**
* The removeElement() method removes the first element in the array that
* satisfies (returns true) the provided testing function.
* @param {Array} array The array.
* @param {Function} fn Function to execute on each value in the array, with the
* function signature <code>fn(element, index, array)</code>. Return true to
* remove this element and break.
* @param {boolean} reverse True to search in reverse order.
* @return {boolean} True if an element was removed.
*/
export function removeElement<T>(
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean,
) {
let i;
let removed;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
removed = array[i];
array.splice(i, 1);
return removed;
}
}
} else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
removed = array[i];
array.splice(i, 1);
return removed;
}
}
}
return false;
}
/**
* Checks if the given thing is a function.
* @param {*} value The thing to check.
* @return {boolean} True if it is a function.
*/
export function isFunction(value: any) {
return Object.prototype.toString.call(value) === "[object Function]";
}
/**
* Checks that the given object has the specified keys.
* @param {Object} obj The object to check.
* @param {string[]} keys The list of keys that 'obj' must have.
* @throws If the object is missing keys.
*/
// note using 'keys' here would shadow the 'keys' function defined above
export function checkObjectHasKeys(obj: object, keys_: string[]) {
for (let i = 0; i < keys_.length; i++) {
if (!obj.hasOwnProperty(keys_[i])) {
throw new Error("Missing required key: " + keys_[i]);
}
}
}
/**
* Checks that the given object has no extra keys other than the specified ones.
* @param {Object} obj The object to check.
* @param {string[]} allowedKeys The list of allowed key names.
* @throws If there are extra keys.
*/
export function checkObjectHasNoAdditionalKeys(obj: object, allowedKeys: string[]): void {
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
if (allowedKeys.indexOf(key) === -1) {
throw new Error("Unknown key: " + key);
}
}
}
/**
* Deep copy the given object. The object MUST NOT have circular references and
* MUST NOT have functions.
* @param {Object} obj The object to deep copy.
* @return {Object} A copy of the object without any references to the original.
*/
export function deepCopy<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
/**
* Compare two objects for equality. The objects MUST NOT have circular references.
*
* @param {Object} x The first object to compare.
* @param {Object} y The second object to compare.
*
* @return {boolean} true if the two objects are equal
*/
export function deepCompare(x: any, y: any): boolean {
// Inspired by
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
// Compare primitives and functions.
// Also check if both arguments link to the same object.
if (x === y) {
return true;
}
if (typeof x !== typeof y) {
return false;
}
// special-case NaN (since NaN !== NaN)
if (typeof x === 'number' && isNaN(x) && isNaN(y)) {
return true;
}
// special-case null (since typeof null == 'object', but null.constructor
// throws)
if (x === null || y === null) {
return x === y;
}
// everything else is either an unequal primitive, or an object
if (!(x instanceof Object)) {
return false;
}
// check they are the same type of object
if (x.constructor !== y.constructor || x.prototype !== y.prototype) {
return false;
}
// special-casing for some special types of object
if (x instanceof RegExp || x instanceof Date) {
return x.toString() === y.toString();
}
// the object algorithm works for Array, but it's sub-optimal.
if (x instanceof Array) {
if (x.length !== y.length) {
return false;
}
for (let i = 0; i < x.length; i++) {
if (!deepCompare(x[i], y[i])) {
return false;
}
}
} else {
// disable jshint "The body of a for in should be wrapped in an if
// statement"
/* jshint -W089 */
// check that all of y's direct keys are in x
let p;
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
}
// finally, compare each of x's keys with y
for (p in y) { // eslint-disable-line guard-for-in
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
if (!deepCompare(x[p], y[p])) {
return false;
}
}
}
/* jshint +W089 */
return true;
}
/**
* Copy properties from one object to another.
*
* All enumerable properties, included inherited ones, are copied.
*
* This is approximately equivalent to ES6's Object.assign, except
* that the latter doesn't copy inherited properties.
*
* @param {Object} target The object that will receive new properties
* @param {...Object} source Objects from which to copy properties
*
* @return {Object} target
*/
export function extend(...restParams) {
const target = restParams[0] || {};
for (let i = 1; i < restParams.length; i++) {
const source = restParams[i];
if (!source) continue;
for (const propName in source) { // eslint-disable-line guard-for-in
target[propName] = source[propName];
}
}
return target;
}
/**
* Inherit the prototype methods from one constructor into another. This is a
* port of the Node.js implementation with an Object.create polyfill.
*
* @param {function} ctor Constructor function which needs to inherit the
* prototype.
* @param {function} superCtor Constructor function to inherit prototype from.
*/
export function inherits(ctor: Function, superCtor: Function) {
// Add util.inherits from Node.js
// Source:
// https://github.com/joyent/node/blob/master/lib/util.js
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
(ctor as any).super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true,
},
});
}
/**
* Polyfills inheritance for prototypes by allowing different kinds of
* super types. Typically prototypes would use `SuperType.call(this, params)`
* though this doesn't always work in some environments - this function
* falls back to using `Object.assign()` to clone a constructed copy
* of the super type onto `thisArg`.
* @param {any} thisArg The child instance. Modified in place.
* @param {any} SuperType The type to act as a super instance
* @param {any} params Arguments to supply to the super type's constructor
*/
export function polyfillSuper(thisArg: any, SuperType: any, ...params: any[]) {
try {
SuperType.call(thisArg, ...params);
} catch (e) {
// fall back to Object.assign to just clone the thing
const fakeSuper = new SuperType(...params);
Object.assign(thisArg, fakeSuper);
}
}
/**
* Returns whether the given value is a finite number without type-coercion
*
* @param {*} value the value to test
* @return {boolean} whether or not value is a finite number without type-coercion
*/
export function isNumber(value: any): boolean {
return typeof value === 'number' && isFinite(value);
}
/**
* Removes zero width chars, diacritics and whitespace from the string
* Also applies an unhomoglyph on the string, to prevent similar looking chars
* @param {string} str the string to remove hidden characters from
* @return {string} a string with the hidden characters removed
*/
export function removeHiddenChars(str: string): string {
if (typeof str === "string") {
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
}
return "";
}
export function normalize(str: string): string {
// Note: we have to match the filter with the removeHiddenChars() because the
// function strips spaces and other characters (M becomes RN for example, in lowercase).
return removeHiddenChars(str.toLowerCase())
// Strip all punctuation
.replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "")
// We also doubly convert to lowercase to work around oddities of the library.
.toLowerCase();
}
// Regex matching bunch of unicode control characters and otherwise misleading/invisible characters.
// Includes:
// various width spaces U+2000 - U+200D
// LTR and RTL marks U+200E and U+200F
// LTR/RTL and other directional formatting marks U+202A - U+202F
// Combining characters U+0300 - U+036F
// Zero width no-break space (BOM) U+FEFF
// eslint-disable-next-line no-misleading-character-class
const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g;
export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function globToRegexp(glob: string, extended: any): string {
extended = typeof(extended) === 'boolean' ? extended : true;
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/g, '.*');
pat = pat.replace(/\?/g, '.');
if (extended) {
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
const first = p1 && '^' || '';
const second = p2.replace(/\\-/, '-');
return '[' + first + second + ']';
});
}
return pat;
}
export function ensureNoTrailingSlash(url: string): string {
if (url && url.endsWith("/")) {
return url.substr(0, url.length - 1);
} else {
return url;
}
}
// Returns a promise which resolves with a given value after the given number of ms
export function sleep<T>(ms: number, value?: T): Promise<T> {
return new Promise((resolve => {
setTimeout(resolve, ms, value);
}));
}
export function isNullOrUndefined(val: any): boolean {
return val === null || val === undefined;
}
// Returns a Deferred
export function defer() {
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
return { resolve, reject, promise };
}
export async function promiseMapSeries<T>(
promises: Promise<T>[],
fn: (t: T) => void,
): Promise<void> {
for (const o of await promises) {
await fn(await o);
}
}
export function promiseTry<T>(fn: () => T): Promise<T> {
return new Promise((resolve) => resolve(fn()));
}
// Creates and awaits all promises, running no more than `chunkSize` at the same time
export async function chunkPromises<T>(fns: (() => Promise<T>)[], chunkSize: number): Promise<T[]> {
const results: T[] = [];
for (let i = 0; i < fns.length; i += chunkSize) {
results.push(...(await Promise.all(fns.slice(i, i + chunkSize).map(fn => fn()))));
}
return results;
}
// We need to be able to access the Node.js crypto library from within the
// Matrix SDK without needing to `require("crypto")`, which will fail in
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
// it, we can call `getCrypto`.
let crypto: Object;
export function setCrypto(c: Object) {
crypto = c;
}
export function getCrypto(): Object {
return crypto;
}
// String averaging based upon https://stackoverflow.com/a/2510816
// Dev note: We make the alphabet a string because it's easier to write syntactically
// than arrays. Thankfully, strings implement the useful parts of the Array interface
// anyhow.
/**
* The default alphabet used by string averaging in this SDK. This matches
* all usefully printable ASCII characters (0x20-0x7E, inclusive).
*/
export const DEFAULT_ALPHABET = [""].reduce(() => {
let str = "";
for (let c = 0x20; c <= 0x7E; c++) {
str += String.fromCharCode(c);
}
return str;
}, "");
/**
* Pads a string using the given alphabet as a base. The returned string will be the
* same length as the alphabet, and padded with the first character in the alphabet.
*
* This is intended for use with string averaging.
* @param {string} s The string to pad.
* @param {string} alphabet The alphabet to use as a single string.
* @returns {string} The padded string.
*/
export function alphabetPad(s: string, alphabet = DEFAULT_ALPHABET): string {
while (s.length < alphabet.length) {
s = alphabet[0] + s;
}
return s;
}
/**
* Converts a baseN number to a string, where N is the alphabet's length.
*
* This is intended for use with string averaging.
* @param {number} n The baseN number.
* @param {string} alphabet The alphabet to use as a single string.
* @returns {string} The baseN number encoded as a string from the alphabet.
*/
export function baseToString(n: number, alphabet = DEFAULT_ALPHABET): string {
const len = alphabet.length;
if (n < len) {
return alphabet[n];
}
return baseToString(Math.floor(n / len), alphabet) + alphabet[n % len];
}
/**
* Converts a string to a baseN number, where N is the alphabet's length.
*
* This is intended for use with string averaging.
* @param {string} s The string to convert to a number.
* @param {string} alphabet The alphabet to use as a single string.
* @returns {number} The baseN number.
*/
export function stringToBase(s: string, alphabet = DEFAULT_ALPHABET): number {
s = alphabetPad(s, alphabet);
const len = alphabet.length;
const reversedStr = Array.from(s).reverse();
let result = 0;
for (let i = 0; i < len; i++) {
result += alphabet.indexOf(reversedStr[i]) * (len ** i);
}
return result;
}
/**
* Averages two strings, returning the midpoint between them. This is accomplished by
* converting both to baseN numbers (where N is the alphabet's length) then averaging
* those before re-encoding as a string.
* @param {string} a The first string.
* @param {string} b The second string.
* @param {string} alphabet The alphabet to use as a single string.
* @returns {string} The midpoint between the strings, as a string.
*/
export function averageBetweenStrings(a: string, b: string, alphabet = DEFAULT_ALPHABET): string {
return baseToString(Math.floor((stringToBase(a, alphabet) + stringToBase(b, alphabet)) / 2), alphabet);
}
/**
* Finds the next string using the alphabet provided. This is done by converting the
* string to a baseN number, where N is the alphabet's length, then adding 1 before
* converting back to a string.
* @param {string} s The string to start at.
* @param {string} alphabet The alphabet to use as a single string.
* @returns {string} The string which follows the input string.
*/
export function nextString(s: string, alphabet = DEFAULT_ALPHABET): string {
return baseToString(stringToBase(s, alphabet) + 1, alphabet);
}
/**
* Finds the previous string using the alphabet provided. This is done by converting the
* string to a baseN number, where N is the alphabet's length, then subtracting 1 before
* converting back to a string.
* @param {string} s The string to start at.
* @param {string} alphabet The alphabet to use as a single string.
* @returns {string} The string which precedes the input string.
*/
export function prevString(s: string, alphabet = DEFAULT_ALPHABET): string {
return baseToString(stringToBase(s, alphabet) - 1, alphabet);
}