2012-11-30 18:33:32 +01:00
|
|
|
// From MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random
|
|
|
|
exports.random_int = function random_int(min, max) {
|
|
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
|
|
};
|
|
|
|
|
2013-02-20 00:49:21 +01:00
|
|
|
// Like C++'s std::lower_bound. Returns the first index at which
|
|
|
|
// `value` could be inserted without changing the ordering. Assumes
|
|
|
|
// the array is sorted.
|
|
|
|
//
|
|
|
|
// `first` and `last` are indices and `less` is an optionally-specified
|
|
|
|
// function that returns true if
|
|
|
|
// array[i] < value
|
|
|
|
// for some i and false otherwise.
|
|
|
|
//
|
|
|
|
// Usage: lower_bound(array, value, [less])
|
|
|
|
// lower_bound(array, first, last, value, [less])
|
|
|
|
exports.lower_bound = function (array, arg1, arg2, arg3, arg4) {
|
2019-11-02 00:06:25 +01:00
|
|
|
let first;
|
|
|
|
let last;
|
|
|
|
let value;
|
|
|
|
let less;
|
2013-02-20 00:49:21 +01:00
|
|
|
if (arg3 === undefined) {
|
|
|
|
first = 0;
|
|
|
|
last = array.length;
|
|
|
|
value = arg1;
|
|
|
|
less = arg2;
|
|
|
|
} else {
|
|
|
|
first = arg1;
|
|
|
|
last = arg2;
|
|
|
|
value = arg3;
|
|
|
|
less = arg4;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (less === undefined) {
|
|
|
|
less = function (a, b) { return a < b; };
|
|
|
|
}
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
let len = last - first;
|
|
|
|
let middle;
|
|
|
|
let step;
|
2013-02-20 00:49:21 +01:00
|
|
|
while (len > 0) {
|
|
|
|
step = Math.floor(len / 2);
|
|
|
|
middle = first + step;
|
2014-03-11 20:17:33 +01:00
|
|
|
if (less(array[middle], value, middle)) {
|
2013-02-20 00:49:21 +01:00
|
|
|
first = middle;
|
2016-11-30 19:05:04 +01:00
|
|
|
first += 1;
|
2013-02-20 00:49:21 +01:00
|
|
|
len = len - step - 1;
|
2016-06-09 23:02:49 +02:00
|
|
|
} else {
|
2013-02-20 00:49:21 +01:00
|
|
|
len = step;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return first;
|
|
|
|
};
|
|
|
|
|
2018-12-22 16:10:16 +01:00
|
|
|
function lower_same(a, b) {
|
|
|
|
return a.toLowerCase() === b.toLowerCase();
|
|
|
|
}
|
|
|
|
|
2016-08-27 04:13:35 +02:00
|
|
|
exports.same_stream_and_topic = function util_same_stream_and_topic(a, b) {
|
|
|
|
// Streams and topics are case-insensitive.
|
2018-06-06 18:50:09 +02:00
|
|
|
return a.stream_id === b.stream_id &&
|
2020-02-19 00:04:12 +01:00
|
|
|
lower_same(a.topic, b.topic);
|
2013-02-26 23:09:15 +01:00
|
|
|
};
|
|
|
|
|
2020-06-05 19:49:16 +02:00
|
|
|
exports.is_pm_recipient = function (user_id, message) {
|
|
|
|
const recipients = message.to_user_ids.split(',');
|
|
|
|
return recipients.includes(user_id.toString());
|
2016-06-08 06:06:57 +02:00
|
|
|
};
|
|
|
|
|
2016-06-10 20:15:08 +02:00
|
|
|
exports.extract_pm_recipients = function (recipients) {
|
2020-02-08 03:33:46 +01:00
|
|
|
return recipients.split(/\s*[,;]\s*/).filter(recipient => recipient.trim() !== "");
|
2016-06-10 20:15:08 +02:00
|
|
|
};
|
|
|
|
|
2013-02-26 23:09:15 +01:00
|
|
|
exports.same_recipient = function util_same_recipient(a, b) {
|
2018-06-06 18:50:09 +02:00
|
|
|
if (a === undefined || b === undefined) {
|
2013-02-26 23:09:15 +01:00
|
|
|
return false;
|
2013-08-01 17:47:48 +02:00
|
|
|
}
|
|
|
|
if (a.type !== b.type) {
|
2013-02-26 23:09:15 +01:00
|
|
|
return false;
|
2013-08-01 17:47:48 +02:00
|
|
|
}
|
2013-02-26 23:09:15 +01:00
|
|
|
|
|
|
|
switch (a.type) {
|
|
|
|
case 'private':
|
2017-02-25 00:57:09 +01:00
|
|
|
if (a.to_user_ids === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return a.to_user_ids === b.to_user_ids;
|
2013-02-26 23:09:15 +01:00
|
|
|
case 'stream':
|
2016-08-27 04:13:35 +02:00
|
|
|
return exports.same_stream_and_topic(a, b);
|
2013-02-26 23:09:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// should never get here
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.same_sender = function util_same_sender(a, b) {
|
2018-06-06 18:50:09 +02:00
|
|
|
return a !== undefined && b !== undefined &&
|
|
|
|
a.sender_email.toLowerCase() === b.sender_email.toLowerCase();
|
2013-02-26 23:09:15 +01:00
|
|
|
};
|
|
|
|
|
2013-06-17 20:31:31 +02:00
|
|
|
exports.normalize_recipients = function (recipients) {
|
2013-07-26 20:27:06 +02:00
|
|
|
// Converts a string listing emails of message recipients
|
|
|
|
// into a canonical formatting: emails sorted ASCIIbetically
|
|
|
|
// with exactly one comma and no spaces between each.
|
js: Convert _.map(a, …) to a.map(…).
And convert the corresponding function expressions to arrow style
while we’re here.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import K from "ast-types/gen/kinds";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
recast.visit(ast, {
visitCallExpression(path) {
const { callee, arguments: args } = path.node;
if (
n.MemberExpression.check(callee) &&
!callee.computed &&
n.Identifier.check(callee.object) &&
callee.object.name === "_" &&
n.Identifier.check(callee.property) &&
callee.property.name === "map" &&
args.length === 2 &&
checkExpression(args[0]) &&
checkExpression(args[1])
) {
const [arr, fn] = args;
path.replace(
b.callExpression(b.memberExpression(arr, b.identifier("map")), [
n.FunctionExpression.check(fn) ||
n.ArrowFunctionExpression.check(fn)
? b.arrowFunctionExpression(
fn.params,
n.BlockStatement.check(fn.body) &&
fn.body.body.length === 1 &&
n.ReturnStatement.check(fn.body.body[0])
? fn.body.body[0].argument || b.identifier("undefined")
: fn.body
)
: fn,
])
);
changed = true;
}
this.traverse(path);
},
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-08 02:43:49 +01:00
|
|
|
recipients = recipients.split(',').map(s => s.trim());
|
|
|
|
recipients = recipients.map(s => s.toLowerCase());
|
2020-02-08 03:33:46 +01:00
|
|
|
recipients = recipients.filter(s => s.length > 0);
|
2013-06-17 20:31:31 +02:00
|
|
|
recipients.sort();
|
|
|
|
return recipients.join(',');
|
|
|
|
};
|
|
|
|
|
2013-03-28 22:32:17 +01:00
|
|
|
// Avoid URI decode errors by removing characters from the end
|
|
|
|
// one by one until the decode succeeds. This makes sense if
|
|
|
|
// we are decoding input that the user is in the middle of
|
|
|
|
// typing.
|
|
|
|
exports.robust_uri_decode = function (str) {
|
2019-11-02 00:06:25 +01:00
|
|
|
let end = str.length;
|
2013-03-28 22:32:17 +01:00
|
|
|
while (end > 0) {
|
|
|
|
try {
|
|
|
|
return decodeURIComponent(str.substring(0, end));
|
|
|
|
} catch (e) {
|
2013-08-01 17:47:48 +02:00
|
|
|
if (!(e instanceof URIError)) {
|
2013-03-28 22:32:17 +01:00
|
|
|
throw e;
|
2013-08-01 17:47:48 +02:00
|
|
|
}
|
2016-11-30 19:05:04 +01:00
|
|
|
end -= 1;
|
2013-03-28 22:32:17 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return '';
|
|
|
|
};
|
|
|
|
|
2013-05-03 19:16:50 +02:00
|
|
|
// If we can, use a locale-aware sorter. However, if the browser
|
|
|
|
// doesn't support the ECMAScript Internationalization API
|
|
|
|
// Specification, do a dumb string comparison because
|
|
|
|
// String.localeCompare is really slow.
|
2017-06-03 23:55:52 +02:00
|
|
|
exports.make_strcmp = function () {
|
2013-05-03 19:16:50 +02:00
|
|
|
try {
|
2019-11-02 00:06:25 +01:00
|
|
|
const collator = new Intl.Collator();
|
2013-05-03 19:16:50 +02:00
|
|
|
return collator.compare;
|
|
|
|
} catch (e) {
|
2016-12-05 02:35:14 +01:00
|
|
|
// continue regardless of error
|
2013-05-03 19:16:50 +02:00
|
|
|
}
|
|
|
|
|
2016-12-05 07:02:18 +01:00
|
|
|
return function util_strcmp(a, b) {
|
2018-06-06 18:19:09 +02:00
|
|
|
return a < b ? -1 : a > b ? 1 : 0;
|
2013-05-03 19:16:50 +02:00
|
|
|
};
|
2017-06-03 23:55:52 +02:00
|
|
|
};
|
|
|
|
exports.strcmp = exports.make_strcmp();
|
2013-05-03 19:16:50 +02:00
|
|
|
|
2013-07-05 17:43:56 +02:00
|
|
|
exports.escape_regexp = function (string) {
|
2013-07-01 23:51:03 +02:00
|
|
|
// code from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
|
|
|
// Modified to escape the ^ to appease jslint. :/
|
|
|
|
return string.replace(/([.*+?\^=!:${}()|\[\]\/\\])/g, "\\$1");
|
|
|
|
};
|
|
|
|
|
2013-05-07 19:07:05 +02:00
|
|
|
exports.array_compare = function util_array_compare(a, b) {
|
|
|
|
if (a.length !== b.length) {
|
|
|
|
return false;
|
|
|
|
}
|
2019-11-02 00:06:25 +01:00
|
|
|
let i;
|
2016-11-30 19:05:04 +01:00
|
|
|
for (i = 0; i < a.length; i += 1) {
|
2013-05-07 19:07:05 +02:00
|
|
|
if (a[i] !== b[i]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2013-08-06 21:34:43 +02:00
|
|
|
/* Represents a value that is expensive to compute and should be
|
|
|
|
* computed on demand and then cached. The value can be forcefully
|
|
|
|
* recalculated on the next call to get() by calling reset().
|
|
|
|
*
|
|
|
|
* You must supply a option to the constructor called compute_value
|
|
|
|
* which should be a function that computes the uncached value.
|
|
|
|
*/
|
2019-11-02 00:06:25 +01:00
|
|
|
const unassigned_value_sentinel = {};
|
2013-08-06 21:34:43 +02:00
|
|
|
exports.CachedValue = function (opts) {
|
|
|
|
this._value = unassigned_value_sentinel;
|
2020-02-09 04:15:38 +01:00
|
|
|
Object.assign(this, opts);
|
2013-08-06 21:34:43 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
exports.CachedValue.prototype = {
|
|
|
|
get: function CachedValue_get() {
|
|
|
|
if (this._value === unassigned_value_sentinel) {
|
|
|
|
this._value = this.compute_value();
|
|
|
|
}
|
|
|
|
return this._value;
|
|
|
|
},
|
|
|
|
|
|
|
|
reset: function CachedValue_reset() {
|
|
|
|
this._value = unassigned_value_sentinel;
|
2017-01-12 00:17:43 +01:00
|
|
|
},
|
2013-08-06 21:34:43 +02:00
|
|
|
};
|
2013-07-11 23:42:44 +02:00
|
|
|
|
2020-01-23 07:22:26 +01:00
|
|
|
exports.find_wildcard_mentions = function (message_content) {
|
|
|
|
const mention = message_content.match(/(^|\s)(@\*{2}(all|everyone|stream)\*{2})($|\s)/);
|
|
|
|
if (mention === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return mention[3];
|
2016-08-09 22:38:45 +02:00
|
|
|
};
|
|
|
|
|
2016-12-13 20:03:23 +01:00
|
|
|
exports.move_array_elements_to_front = function util_move_array_elements_to_front(array, selected) {
|
2020-02-12 07:55:57 +01:00
|
|
|
const selected_hash = new Set(selected);
|
2019-11-02 00:06:25 +01:00
|
|
|
const selected_elements = [];
|
|
|
|
const unselected_elements = [];
|
2020-02-12 07:55:57 +01:00
|
|
|
for (const element of array) {
|
|
|
|
(selected_hash.has(element) ? selected_elements : unselected_elements).push(element);
|
2016-12-13 20:03:23 +01:00
|
|
|
}
|
2020-02-12 07:55:57 +01:00
|
|
|
return [...selected_elements, ...unselected_elements];
|
2016-12-13 20:03:23 +01:00
|
|
|
};
|
|
|
|
|
2017-03-09 02:34:45 +01:00
|
|
|
// check by the userAgent string if a user's client is likely mobile.
|
|
|
|
exports.is_mobile = function () {
|
2019-11-02 00:06:25 +01:00
|
|
|
const regex = "Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini";
|
2017-03-09 02:34:45 +01:00
|
|
|
return new RegExp(regex, "i").test(window.navigator.userAgent);
|
|
|
|
};
|
|
|
|
|
2018-04-25 22:55:32 +02:00
|
|
|
function to_int(s) {
|
|
|
|
return parseInt(s, 10);
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.sorted_ids = function (ids) {
|
|
|
|
// This mapping makes sure we are using ints, and
|
|
|
|
// it also makes sure we don't mutate the list.
|
js: Convert _.map(a, …) to a.map(…).
And convert the corresponding function expressions to arrow style
while we’re here.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import K from "ast-types/gen/kinds";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
recast.visit(ast, {
visitCallExpression(path) {
const { callee, arguments: args } = path.node;
if (
n.MemberExpression.check(callee) &&
!callee.computed &&
n.Identifier.check(callee.object) &&
callee.object.name === "_" &&
n.Identifier.check(callee.property) &&
callee.property.name === "map" &&
args.length === 2 &&
checkExpression(args[0]) &&
checkExpression(args[1])
) {
const [arr, fn] = args;
path.replace(
b.callExpression(b.memberExpression(arr, b.identifier("map")), [
n.FunctionExpression.check(fn) ||
n.ArrowFunctionExpression.check(fn)
? b.arrowFunctionExpression(
fn.params,
n.BlockStatement.check(fn.body) &&
fn.body.body.length === 1 &&
n.ReturnStatement.check(fn.body.body[0])
? fn.body.body[0].argument || b.identifier("undefined")
: fn.body
)
: fn,
])
);
changed = true;
}
this.traverse(path);
},
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-08 02:43:49 +01:00
|
|
|
let id_list = ids.map(to_int);
|
2018-04-25 22:55:32 +02:00
|
|
|
id_list.sort(function (a, b) {
|
|
|
|
return a - b;
|
|
|
|
});
|
|
|
|
id_list = _.uniq(id_list, true);
|
|
|
|
|
|
|
|
return id_list;
|
|
|
|
};
|
|
|
|
|
2018-11-15 16:59:41 +01:00
|
|
|
exports.set_match_data = function (target, source) {
|
|
|
|
target.match_subject = source.match_subject;
|
|
|
|
target.match_content = source.match_content;
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.get_match_topic = function (obj) {
|
|
|
|
return obj.match_subject;
|
|
|
|
};
|
|
|
|
|
2018-12-16 17:36:41 +01:00
|
|
|
exports.get_draft_topic = function (obj) {
|
|
|
|
// We will need to support subject for old drafts.
|
|
|
|
return obj.topic || obj.subject || '';
|
|
|
|
};
|
|
|
|
|
2018-12-16 18:02:16 +01:00
|
|
|
exports.get_reload_topic = function (obj) {
|
|
|
|
// When we first upgrade to releases that have
|
|
|
|
// topic=foo in the code, the user's reload URL
|
|
|
|
// may still have subject=foo from the prior version.
|
|
|
|
return obj.topic || obj.subject || '';
|
|
|
|
};
|
|
|
|
|
2018-12-22 17:45:18 +01:00
|
|
|
exports.get_edit_event_topic = function (obj) {
|
2018-12-23 18:08:37 +01:00
|
|
|
if (obj.topic === undefined) {
|
|
|
|
return obj.subject;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This code won't be reachable till we fix the
|
|
|
|
// server, but we use it now in tests.
|
|
|
|
return obj.topic;
|
2018-12-22 17:45:18 +01:00
|
|
|
};
|
|
|
|
|
2018-12-22 17:39:37 +01:00
|
|
|
exports.get_edit_event_orig_topic = function (obj) {
|
|
|
|
return obj.orig_subject;
|
|
|
|
};
|
|
|
|
|
2019-02-22 15:08:16 +01:00
|
|
|
exports.get_edit_event_prev_topic = function (obj) {
|
|
|
|
return obj.prev_subject;
|
|
|
|
};
|
|
|
|
|
2018-12-22 15:32:14 +01:00
|
|
|
exports.is_topic_synonym = function (operator) {
|
|
|
|
return operator === 'subject';
|
|
|
|
};
|
|
|
|
|
2018-12-23 16:49:14 +01:00
|
|
|
exports.convert_message_topic = function (message) {
|
|
|
|
if (message.topic === undefined) {
|
|
|
|
message.topic = message.subject;
|
|
|
|
}
|
|
|
|
};
|
2020-02-28 23:59:07 +01:00
|
|
|
|
|
|
|
exports.clean_user_content_links = function (html) {
|
|
|
|
const content = new DOMParser().parseFromString(html, "text/html").body;
|
|
|
|
for (const elt of content.getElementsByTagName("a")) {
|
|
|
|
// Ensure that all external links have target="_blank"
|
|
|
|
// rel="opener noreferrer". This ensures that external links
|
|
|
|
// never replace the Zulip webapp while also protecting
|
|
|
|
// against reverse tabnapping attacks, without relying on the
|
|
|
|
// correctness of how Zulip's markdown processor generates links.
|
|
|
|
//
|
|
|
|
// Fragment links, which we intend to only open within the
|
|
|
|
// Zulip webapp using our hashchange system, do not require
|
|
|
|
// these attributes.
|
2020-05-09 01:52:39 +02:00
|
|
|
const href = elt.getAttribute("href");
|
2020-02-28 23:59:07 +01:00
|
|
|
let url;
|
|
|
|
try {
|
2020-05-09 01:52:39 +02:00
|
|
|
url = new URL(href, window.location.href);
|
2020-02-28 23:59:07 +01:00
|
|
|
} catch {
|
|
|
|
elt.removeAttribute("href");
|
2020-05-09 01:52:39 +02:00
|
|
|
elt.removeAttribute("title");
|
2020-02-28 23:59:07 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-05-09 01:52:39 +02:00
|
|
|
// eslint-disable-next-line no-script-url
|
|
|
|
if (["data:", "javascript:", "vbscript:"].includes(url.protocol)) {
|
2020-03-24 03:53:15 +01:00
|
|
|
// Remove unsafe links completely.
|
|
|
|
elt.removeAttribute("href");
|
2020-05-09 01:52:39 +02:00
|
|
|
elt.removeAttribute("title");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We detect URLs that are just fragments by comparing the URL
|
|
|
|
// against a new URL generated using only the hash.
|
|
|
|
if (url.hash === "" || url.href !== new URL(url.hash, window.location.href).href) {
|
2020-02-28 23:59:07 +01:00
|
|
|
elt.setAttribute("target", "_blank");
|
|
|
|
elt.setAttribute("rel", "noopener noreferrer");
|
|
|
|
} else {
|
|
|
|
elt.removeAttribute("target");
|
|
|
|
}
|
2020-05-09 01:52:39 +02:00
|
|
|
|
|
|
|
// Ensure that the title displays the real URL.
|
|
|
|
let title;
|
|
|
|
let legacy_title;
|
|
|
|
if (url.origin === window.location.origin && url.pathname.startsWith("/user_uploads/")) {
|
|
|
|
title = legacy_title = url.pathname.slice(url.pathname.lastIndexOf("/") + 1);
|
|
|
|
} else {
|
|
|
|
title = url;
|
|
|
|
legacy_title = href;
|
|
|
|
}
|
|
|
|
elt.setAttribute("title", ["", legacy_title].includes(elt.title) ? title : `${title}\n${elt.title}`);
|
2020-02-28 23:59:07 +01:00
|
|
|
}
|
|
|
|
return content.innerHTML;
|
|
|
|
};
|