UNPKG

23.6 kBJavaScriptView Raw
1/*
2Copyright 2015, 2016 OpenMarket Ltd
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16"use strict";
17/**
18 * This is an internal module.
19 * @module utils
20 */
21
22const unhomoglyph = require('unhomoglyph');
23
24/**
25 * Encode a dictionary of query parameters.
26 * @param {Object} params A dict of key/values to encode e.g.
27 * {"foo": "bar", "baz": "taz"}
28 * @return {string} The encoded string e.g. foo=bar&baz=taz
29 */
30module.exports.encodeParams = function(params) {
31 let qs = "";
32 for (const key in params) {
33 if (!params.hasOwnProperty(key)) {
34 continue;
35 }
36 qs += "&" + encodeURIComponent(key) + "=" +
37 encodeURIComponent(params[key]);
38 }
39 return qs.substring(1);
40};
41
42/**
43 * Encodes a URI according to a set of template variables. Variables will be
44 * passed through encodeURIComponent.
45 * @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
46 * @param {Object} variables The key/value pairs to replace the template
47 * variables with. E.g. { "$bar": "baz" }.
48 * @return {string} The result of replacing all template variables e.g. '/foo/baz'.
49 */
50module.exports.encodeUri = function(pathTemplate, variables) {
51 for (const key in variables) {
52 if (!variables.hasOwnProperty(key)) {
53 continue;
54 }
55 pathTemplate = pathTemplate.replace(
56 key, encodeURIComponent(variables[key]),
57 );
58 }
59 return pathTemplate;
60};
61
62/**
63 * Applies a map function to the given array.
64 * @param {Array} array The array to apply the function to.
65 * @param {Function} fn The function that will be invoked for each element in
66 * the array with the signature <code>fn(element){...}</code>
67 * @return {Array} A new array with the results of the function.
68 */
69module.exports.map = function(array, fn) {
70 const results = new Array(array.length);
71 for (let i = 0; i < array.length; i++) {
72 results[i] = fn(array[i]);
73 }
74 return results;
75};
76
77/**
78 * Applies a filter function to the given array.
79 * @param {Array} array The array to apply the function to.
80 * @param {Function} fn The function that will be invoked for each element in
81 * the array. It should return true to keep the element. The function signature
82 * looks like <code>fn(element, index, array){...}</code>.
83 * @return {Array} A new array with the results of the function.
84 */
85module.exports.filter = function(array, fn) {
86 const results = [];
87 for (let i = 0; i < array.length; i++) {
88 if (fn(array[i], i, array)) {
89 results.push(array[i]);
90 }
91 }
92 return results;
93};
94
95/**
96 * Get the keys for an object. Same as <code>Object.keys()</code>.
97 * @param {Object} obj The object to get the keys for.
98 * @return {string[]} The keys of the object.
99 */
100module.exports.keys = function(obj) {
101 const keys = [];
102 for (const key in obj) {
103 if (!obj.hasOwnProperty(key)) {
104 continue;
105 }
106 keys.push(key);
107 }
108 return keys;
109};
110
111/**
112 * Get the values for an object.
113 * @param {Object} obj The object to get the values for.
114 * @return {Array<*>} The values of the object.
115 */
116module.exports.values = function(obj) {
117 const values = [];
118 for (const key in obj) {
119 if (!obj.hasOwnProperty(key)) {
120 continue;
121 }
122 values.push(obj[key]);
123 }
124 return values;
125};
126
127/**
128 * Invoke a function for each item in the array.
129 * @param {Array} array The array.
130 * @param {Function} fn The function to invoke for each element. Has the
131 * function signature <code>fn(element, index)</code>.
132 */
133module.exports.forEach = function(array, fn) {
134 for (let i = 0; i < array.length; i++) {
135 fn(array[i], i);
136 }
137};
138
139/**
140 * The findElement() method returns a value in the array, if an element in the array
141 * satisfies (returns true) the provided testing function. Otherwise undefined
142 * is returned.
143 * @param {Array} array The array.
144 * @param {Function} fn Function to execute on each value in the array, with the
145 * function signature <code>fn(element, index, array)</code>
146 * @param {boolean} reverse True to search in reverse order.
147 * @return {*} The first value in the array which returns <code>true</code> for
148 * the given function.
149 */
150module.exports.findElement = function(array, fn, reverse) {
151 let i;
152 if (reverse) {
153 for (i = array.length - 1; i >= 0; i--) {
154 if (fn(array[i], i, array)) {
155 return array[i];
156 }
157 }
158 } else {
159 for (i = 0; i < array.length; i++) {
160 if (fn(array[i], i, array)) {
161 return array[i];
162 }
163 }
164 }
165};
166
167/**
168 * The removeElement() method removes the first element in the array that
169 * satisfies (returns true) the provided testing function.
170 * @param {Array} array The array.
171 * @param {Function} fn Function to execute on each value in the array, with the
172 * function signature <code>fn(element, index, array)</code>. Return true to
173 * remove this element and break.
174 * @param {boolean} reverse True to search in reverse order.
175 * @return {boolean} True if an element was removed.
176 */
177module.exports.removeElement = function(array, fn, reverse) {
178 let i;
179 let removed;
180 if (reverse) {
181 for (i = array.length - 1; i >= 0; i--) {
182 if (fn(array[i], i, array)) {
183 removed = array[i];
184 array.splice(i, 1);
185 return removed;
186 }
187 }
188 } else {
189 for (i = 0; i < array.length; i++) {
190 if (fn(array[i], i, array)) {
191 removed = array[i];
192 array.splice(i, 1);
193 return removed;
194 }
195 }
196 }
197 return false;
198};
199
200/**
201 * Checks if the given thing is a function.
202 * @param {*} value The thing to check.
203 * @return {boolean} True if it is a function.
204 */
205module.exports.isFunction = function(value) {
206 return Object.prototype.toString.call(value) == "[object Function]";
207};
208
209/**
210 * Checks if the given thing is an array.
211 * @param {*} value The thing to check.
212 * @return {boolean} True if it is an array.
213 */
214module.exports.isArray = function(value) {
215 return Array.isArray ? Array.isArray(value) :
216 Boolean(value && value.constructor === Array);
217};
218
219/**
220 * Checks that the given object has the specified keys.
221 * @param {Object} obj The object to check.
222 * @param {string[]} keys The list of keys that 'obj' must have.
223 * @throws If the object is missing keys.
224 */
225module.exports.checkObjectHasKeys = function(obj, keys) {
226 for (let i = 0; i < keys.length; i++) {
227 if (!obj.hasOwnProperty(keys[i])) {
228 throw new Error("Missing required key: " + keys[i]);
229 }
230 }
231};
232
233/**
234 * Checks that the given object has no extra keys other than the specified ones.
235 * @param {Object} obj The object to check.
236 * @param {string[]} allowedKeys The list of allowed key names.
237 * @throws If there are extra keys.
238 */
239module.exports.checkObjectHasNoAdditionalKeys = function(obj, allowedKeys) {
240 for (const key in obj) {
241 if (!obj.hasOwnProperty(key)) {
242 continue;
243 }
244 if (allowedKeys.indexOf(key) === -1) {
245 throw new Error("Unknown key: " + key);
246 }
247 }
248};
249
250/**
251 * Deep copy the given object. The object MUST NOT have circular references and
252 * MUST NOT have functions.
253 * @param {Object} obj The object to deep copy.
254 * @return {Object} A copy of the object without any references to the original.
255 */
256module.exports.deepCopy = function(obj) {
257 return JSON.parse(JSON.stringify(obj));
258};
259
260/**
261 * Compare two objects for equality. The objects MUST NOT have circular references.
262 *
263 * @param {Object} x The first object to compare.
264 * @param {Object} y The second object to compare.
265 *
266 * @return {boolean} true if the two objects are equal
267 */
268const deepCompare = module.exports.deepCompare = function(x, y) {
269 // Inspired by
270 // http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
271
272 // Compare primitives and functions.
273 // Also check if both arguments link to the same object.
274 if (x === y) {
275 return true;
276 }
277
278 if (typeof x !== typeof y) {
279 return false;
280 }
281
282 // special-case NaN (since NaN !== NaN)
283 if (typeof x === 'number' && isNaN(x) && isNaN(y)) {
284 return true;
285 }
286
287 // special-case null (since typeof null == 'object', but null.constructor
288 // throws)
289 if (x === null || y === null) {
290 return x === y;
291 }
292
293 // everything else is either an unequal primitive, or an object
294 if (!(x instanceof Object)) {
295 return false;
296 }
297
298 // check they are the same type of object
299 if (x.constructor !== y.constructor || x.prototype !== y.prototype) {
300 return false;
301 }
302
303 // special-casing for some special types of object
304 if (x instanceof RegExp || x instanceof Date) {
305 return x.toString() === y.toString();
306 }
307
308 // the object algorithm works for Array, but it's sub-optimal.
309 if (x instanceof Array) {
310 if (x.length !== y.length) {
311 return false;
312 }
313
314 for (let i = 0; i < x.length; i++) {
315 if (!deepCompare(x[i], y[i])) {
316 return false;
317 }
318 }
319 } else {
320 // disable jshint "The body of a for in should be wrapped in an if
321 // statement"
322 /* jshint -W089 */
323
324 // check that all of y's direct keys are in x
325 let p;
326 for (p in y) {
327 if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
328 return false;
329 }
330 }
331
332 // finally, compare each of x's keys with y
333 for (p in y) { // eslint-disable-line guard-for-in
334 if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
335 return false;
336 }
337 if (!deepCompare(x[p], y[p])) {
338 return false;
339 }
340 }
341 }
342 /* jshint +W089 */
343 return true;
344};
345
346/**
347 * Copy properties from one object to another.
348 *
349 * All enumerable properties, included inherited ones, are copied.
350 *
351 * This is approximately equivalent to ES6's Object.assign, except
352 * that the latter doesn't copy inherited properties.
353 *
354 * @param {Object} target The object that will receive new properties
355 * @param {...Object} source Objects from which to copy properties
356 *
357 * @return {Object} target
358 */
359module.exports.extend = function() {
360 const target = arguments[0] || {};
361 for (let i = 1; i < arguments.length; i++) {
362 const source = arguments[i];
363 for (const propName in source) { // eslint-disable-line guard-for-in
364 target[propName] = source[propName];
365 }
366 }
367 return target;
368};
369
370/**
371 * Run polyfills to add Array.map and Array.filter if they are missing.
372 */
373module.exports.runPolyfills = function() {
374 // Array.prototype.filter
375 // ========================================================
376 // SOURCE:
377 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
378 if (!Array.prototype.filter) {
379 Array.prototype.filter = function(fun/*, thisArg*/) {
380 if (this === void 0 || this === null) {
381 throw new TypeError();
382 }
383
384 const t = Object(this);
385 const len = t.length >>> 0;
386 if (typeof fun !== 'function') {
387 throw new TypeError();
388 }
389
390 const res = [];
391 const thisArg = arguments.length >= 2 ? arguments[1] : void 0;
392 for (let i = 0; i < len; i++) {
393 if (i in t) {
394 const val = t[i];
395
396 // NOTE: Technically this should Object.defineProperty at
397 // the next index, as push can be affected by
398 // properties on Object.prototype and Array.prototype.
399 // But that method's new, and collisions should be
400 // rare, so use the more-compatible alternative.
401 if (fun.call(thisArg, val, i, t)) {
402 res.push(val);
403 }
404 }
405 }
406
407 return res;
408 };
409 }
410
411 // Array.prototype.map
412 // ========================================================
413 // SOURCE:
414 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
415 // Production steps of ECMA-262, Edition 5, 15.4.4.19
416 // Reference: http://es5.github.io/#x15.4.4.19
417 if (!Array.prototype.map) {
418 Array.prototype.map = function(callback, thisArg) {
419 let T, k;
420
421 if (this === null || this === undefined) {
422 throw new TypeError(' this is null or not defined');
423 }
424
425 // 1. Let O be the result of calling ToObject passing the |this|
426 // value as the argument.
427 const O = Object(this);
428
429 // 2. Let lenValue be the result of calling the Get internal
430 // method of O with the argument "length".
431 // 3. Let len be ToUint32(lenValue).
432 const len = O.length >>> 0;
433
434 // 4. If IsCallable(callback) is false, throw a TypeError exception.
435 // See: http://es5.github.com/#x9.11
436 if (typeof callback !== 'function') {
437 throw new TypeError(callback + ' is not a function');
438 }
439
440 // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
441 if (arguments.length > 1) {
442 T = thisArg;
443 }
444
445 // 6. Let A be a new array created as if by the expression new Array(len)
446 // where Array is the standard built-in constructor with that name and
447 // len is the value of len.
448 const A = new Array(len);
449
450 // 7. Let k be 0
451 k = 0;
452
453 // 8. Repeat, while k < len
454 while (k < len) {
455 var kValue, mappedValue;
456
457 // a. Let Pk be ToString(k).
458 // This is implicit for LHS operands of the in operator
459 // b. Let kPresent be the result of calling the HasProperty internal
460 // method of O with argument Pk.
461 // This step can be combined with c
462 // c. If kPresent is true, then
463 if (k in O) {
464 // i. Let kValue be the result of calling the Get internal
465 // method of O with argument Pk.
466 kValue = O[k];
467
468 // ii. Let mappedValue be the result of calling the Call internal
469 // method of callback with T as the this value and argument
470 // list containing kValue, k, and O.
471 mappedValue = callback.call(T, kValue, k, O);
472
473 // iii. Call the DefineOwnProperty internal method of A with arguments
474 // Pk, Property Descriptor
475 // { Value: mappedValue,
476 // Writable: true,
477 // Enumerable: true,
478 // Configurable: true },
479 // and false.
480
481 // In browsers that support Object.defineProperty, use the following:
482 // Object.defineProperty(A, k, {
483 // value: mappedValue,
484 // writable: true,
485 // enumerable: true,
486 // configurable: true
487 // });
488
489 // For best browser support, use the following:
490 A[k] = mappedValue;
491 }
492 // d. Increase k by 1.
493 k++;
494 }
495
496 // 9. return A
497 return A;
498 };
499 }
500
501 // Array.prototype.forEach
502 // ========================================================
503 // SOURCE:
504 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
505 // Production steps of ECMA-262, Edition 5, 15.4.4.18
506 // Reference: http://es5.github.io/#x15.4.4.18
507 if (!Array.prototype.forEach) {
508 Array.prototype.forEach = function(callback, thisArg) {
509 let T, k;
510
511 if (this === null || this === undefined) {
512 throw new TypeError(' this is null or not defined');
513 }
514
515 // 1. Let O be the result of calling ToObject passing the |this| value as the
516 // argument.
517 const O = Object(this);
518
519 // 2. Let lenValue be the result of calling the Get internal method of O with the
520 // argument "length".
521 // 3. Let len be ToUint32(lenValue).
522 const len = O.length >>> 0;
523
524 // 4. If IsCallable(callback) is false, throw a TypeError exception.
525 // See: http://es5.github.com/#x9.11
526 if (typeof callback !== "function") {
527 throw new TypeError(callback + ' is not a function');
528 }
529
530 // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
531 if (arguments.length > 1) {
532 T = thisArg;
533 }
534
535 // 6. Let k be 0
536 k = 0;
537
538 // 7. Repeat, while k < len
539 while (k < len) {
540 var kValue;
541
542 // a. Let Pk be ToString(k).
543 // This is implicit for LHS operands of the in operator
544 // b. Let kPresent be the result of calling the HasProperty internal
545 // method of O with
546 // argument Pk.
547 // This step can be combined with c
548 // c. If kPresent is true, then
549 if (k in O) {
550 // i. Let kValue be the result of calling the Get internal method of O with
551 // argument Pk
552 kValue = O[k];
553
554 // ii. Call the Call internal method of callback with T as the this value and
555 // argument list containing kValue, k, and O.
556 callback.call(T, kValue, k, O);
557 }
558 // d. Increase k by 1.
559 k++;
560 }
561 // 8. return undefined
562 };
563 }
564};
565
566/**
567 * Inherit the prototype methods from one constructor into another. This is a
568 * port of the Node.js implementation with an Object.create polyfill.
569 *
570 * @param {function} ctor Constructor function which needs to inherit the
571 * prototype.
572 * @param {function} superCtor Constructor function to inherit prototype from.
573 */
574module.exports.inherits = function(ctor, superCtor) {
575 // Add Object.create polyfill for IE8
576 // Source:
577 // https://developer.mozilla.org/en-US/docs/Web/JavaScript
578 // /Reference/Global_Objects/Object/create#Polyfill
579 if (typeof Object.create != 'function') {
580 // Production steps of ECMA-262, Edition 5, 15.2.3.5
581 // Reference: http://es5.github.io/#x15.2.3.5
582 Object.create = (function() {
583 // To save on memory, use a shared constructor
584 function Temp() {}
585
586 // make a safe reference to Object.prototype.hasOwnProperty
587 const hasOwn = Object.prototype.hasOwnProperty;
588
589 return function(O) {
590 // 1. If Type(O) is not Object or Null throw a TypeError exception.
591 if (typeof O != 'object') {
592 throw new TypeError('Object prototype may only be an Object or null');
593 }
594
595 // 2. Let obj be the result of creating a new object as if by the
596 // expression new Object() where Object is the standard built-in
597 // constructor with that name
598 // 3. Set the [[Prototype]] internal property of obj to O.
599 Temp.prototype = O;
600 const obj = new Temp();
601 Temp.prototype = null; // Let's not keep a stray reference to O...
602
603 // 4. If the argument Properties is present and not undefined, add
604 // own properties to obj as if by calling the standard built-in
605 // function Object.defineProperties with arguments obj and
606 // Properties.
607 if (arguments.length > 1) {
608 // Object.defineProperties does ToObject on its first argument.
609 const Properties = Object(arguments[1]);
610 for (const prop in Properties) {
611 if (hasOwn.call(Properties, prop)) {
612 obj[prop] = Properties[prop];
613 }
614 }
615 }
616
617 // 5. Return obj
618 return obj;
619 };
620 })();
621 }
622 // END polyfill
623
624 // Add util.inherits from Node.js
625 // Source:
626 // https://github.com/joyent/node/blob/master/lib/util.js
627 // Copyright Joyent, Inc. and other Node contributors.
628 //
629 // Permission is hereby granted, free of charge, to any person obtaining a
630 // copy of this software and associated documentation files (the
631 // "Software"), to deal in the Software without restriction, including
632 // without limitation the rights to use, copy, modify, merge, publish,
633 // distribute, sublicense, and/or sell copies of the Software, and to permit
634 // persons to whom the Software is furnished to do so, subject to the
635 // following conditions:
636 //
637 // The above copyright notice and this permission notice shall be included
638 // in all copies or substantial portions of the Software.
639 //
640 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
641 // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
642 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
643 // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
644 // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
645 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
646 // USE OR OTHER DEALINGS IN THE SOFTWARE.
647 ctor.super_ = superCtor;
648 ctor.prototype = Object.create(superCtor.prototype, {
649 constructor: {
650 value: ctor,
651 enumerable: false,
652 writable: true,
653 configurable: true,
654 },
655 });
656};
657
658/**
659 * Returns whether the given value is a finite number without type-coercion
660 *
661 * @param {*} value the value to test
662 * @return {boolean} whether or not value is a finite number without type-coercion
663 */
664module.exports.isNumber = function(value) {
665 return typeof value === 'number' && isFinite(value);
666};
667
668/**
669 * Removes zero width chars, diacritics and whitespace from the string
670 * Also applies an unhomoglyph on the string, to prevent similar looking chars
671 * @param {string} str the string to remove hidden characters from
672 * @return {string} a string with the hidden characters removed
673 */
674module.exports.removeHiddenChars = function(str) {
675 return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
676};
677const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g;
678
679function escapeRegExp(string) {
680 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
681}
682module.exports.escapeRegExp = escapeRegExp;
683
684module.exports.globToRegexp = function(glob, extended) {
685 extended = typeof(extended) === 'boolean' ? extended : true;
686 // From
687 // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
688 // Because micromatch is about 130KB with dependencies,
689 // and minimatch is not much better.
690 let pat = escapeRegExp(glob);
691 pat = pat.replace(/\\\*/g, '.*');
692 pat = pat.replace(/\?/g, '.');
693 if (extended) {
694 pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
695 const first = p1 && '^' || '';
696 const second = p2.replace(/\\-/, '-');
697 return '[' + first + second + ']';
698 });
699 }
700 return pat;
701};