core.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. // Converse.js (A browser based XMPP chat client)
  2. // https://conversejs.org
  3. //
  4. // This is the utilities module.
  5. //
  6. // Copyright (c) 2013-2019, Jan-Carel Brand <jc@opkode.com>
  7. // Licensed under the Mozilla Public License (MPLv2)
  8. //
  9. import * as strophe from 'strophe.js/src/core';
  10. import Backbone from "backbone";
  11. import _ from "../lodash.noconflict";
  12. import sizzle from "sizzle";
  13. const Strophe = strophe.default.Strophe;
  14. /**
  15. * The utils object
  16. * @namespace u
  17. */
  18. const u = {};
  19. u.logger = Object.assign({
  20. 'debug': _.get(console, 'log') ? console.log.bind(console) : function noop () {},
  21. 'error': _.get(console, 'log') ? console.log.bind(console) : function noop () {},
  22. 'info': _.get(console, 'log') ? console.log.bind(console) : function noop () {},
  23. 'warn': _.get(console, 'log') ? console.log.bind(console) : function noop () {}
  24. }, console);
  25. u.isTagEqual = function (stanza, name) {
  26. if (stanza.nodeTree) {
  27. return u.isTagEqual(stanza.nodeTree, name);
  28. } else if (!(stanza instanceof Element)) {
  29. throw Error(
  30. "isTagEqual called with value which isn't "+
  31. "an element or Strophe.Builder instance");
  32. } else {
  33. return Strophe.isTagEqual(stanza, name);
  34. }
  35. }
  36. const parser = new DOMParser();
  37. const parserErrorNS = parser.parseFromString('invalid', 'text/xml')
  38. .getElementsByTagName("parsererror")[0].namespaceURI;
  39. u.toStanza = function (string) {
  40. const node = parser.parseFromString(string, "text/xml");
  41. if (node.getElementsByTagNameNS(parserErrorNS, 'parsererror').length) {
  42. throw new Error(`Parser Error: ${string}`);
  43. }
  44. return node.firstElementChild;
  45. }
  46. u.isMAMMessage = function (stanza) {
  47. return sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).length > 0;
  48. }
  49. u.isCarbonMessage = function (stanza) {
  50. const xmlns = Strophe.NS.CARBONS;
  51. return sizzle(`message > received[xmlns="${xmlns}"]`, stanza).length > 0 ||
  52. sizzle(`message > sent[xmlns="${xmlns}"]`, stanza).length > 0;
  53. }
  54. u.getLongestSubstring = function (string, candidates) {
  55. function reducer (accumulator, current_value) {
  56. if (string.startsWith(current_value)) {
  57. if (current_value.length > accumulator.length) {
  58. return current_value;
  59. } else {
  60. return accumulator;
  61. }
  62. } else {
  63. return accumulator;
  64. }
  65. }
  66. return candidates.reduce(reducer, '');
  67. }
  68. u.prefixMentions = function (message) {
  69. /* Given a message object, return its text with @ chars
  70. * inserted before the mentioned nicknames.
  71. */
  72. let text = message.get('message');
  73. (message.get('references') || [])
  74. .sort((a, b) => b.begin - a.begin)
  75. .forEach(ref => {
  76. text = `${text.slice(0, ref.begin)}@${text.slice(ref.begin)}`
  77. });
  78. return text;
  79. };
  80. u.isValidJID = function (jid) {
  81. if (_.isString(jid)) {
  82. return _.compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
  83. }
  84. return false;
  85. };
  86. u.isValidMUCJID = function (jid) {
  87. return !jid.startsWith('@') && !jid.endsWith('@');
  88. };
  89. u.isSameBareJID = function (jid1, jid2) {
  90. if (!_.isString(jid1) || !_.isString(jid2)) {
  91. return false;
  92. }
  93. return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
  94. Strophe.getBareJidFromJid(jid2).toLowerCase();
  95. };
  96. u.isNewMessage = function (message) {
  97. /* Given a stanza, determine whether it's a new
  98. * message, i.e. not a MAM archived one.
  99. */
  100. if (message instanceof Element) {
  101. return !(
  102. sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length &&
  103. sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, message).length
  104. );
  105. } else if (message instanceof Backbone.Model) {
  106. message = message.attributes;
  107. }
  108. return !(message['is_delayed'] && message['is_archived']);
  109. };
  110. u.isEmptyMessage = function (attrs) {
  111. if (attrs instanceof Backbone.Model) {
  112. attrs = attrs.attributes;
  113. }
  114. return !attrs['oob_url'] &&
  115. !attrs['file'] &&
  116. !(attrs['is_encrypted'] && attrs['plaintext']) &&
  117. !attrs['message'];
  118. };
  119. u.isOnlyChatStateNotification = function (msg) {
  120. if (msg instanceof Element) {
  121. // See XEP-0085 Chat State Notification
  122. return (msg.querySelector('body') === null) && (
  123. (msg.querySelector('active') !== null) ||
  124. (msg.querySelector('composing') !== null) ||
  125. (msg.querySelector('inactive') !== null) ||
  126. (msg.querySelector('paused') !== null) ||
  127. (msg.querySelector('gone') !== null));
  128. }
  129. if (msg instanceof Backbone.Model) {
  130. msg = msg.attributes;
  131. }
  132. return msg['chat_state'] && u.isEmptyMessage(msg);
  133. };
  134. u.isOnlyMessageDeliveryReceipt = function (msg) {
  135. if (msg instanceof Element) {
  136. // See XEP-0184 Message Delivery Receipts
  137. return (msg.querySelector('body') === null) &&
  138. (msg.querySelector('received') !== null);
  139. }
  140. if (msg instanceof Backbone.Model) {
  141. msg = msg.attributes;
  142. }
  143. return msg['received'] && u.isEmptyMessage(msg);
  144. };
  145. u.isHeadlineMessage = function (_converse, message) {
  146. const from_jid = message.getAttribute('from');
  147. if (message.getAttribute('type') === 'headline') {
  148. return true;
  149. }
  150. const chatbox = _converse.chatboxes.get(Strophe.getBareJidFromJid(from_jid));
  151. if (chatbox && chatbox.get('type') === _converse.CHATROOMS_TYPE) {
  152. return false;
  153. }
  154. if (message.getAttribute('type') !== 'error' && from_jid && !_.includes(from_jid, '@')) {
  155. // Some servers (I'm looking at you Prosody) don't set the message
  156. // type to "headline" when sending server messages. For now we
  157. // check if an @ signal is included, and if not, we assume it's
  158. // a headline message.
  159. return true;
  160. }
  161. return false;
  162. };
  163. u.isErrorObject = function (o) {
  164. return o instanceof Error;
  165. }
  166. u.isErrorStanza = function (stanza) {
  167. if (!_.isElement(stanza)) {
  168. return false;
  169. }
  170. return stanza.getAttribute('type') === 'error';
  171. }
  172. u.isForbiddenError = function (stanza) {
  173. if (!_.isElement(stanza)) {
  174. return false;
  175. }
  176. return sizzle(`error[type="auth"] forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0;
  177. }
  178. u.isServiceUnavailableError = function (stanza) {
  179. if (!_.isElement(stanza)) {
  180. return false;
  181. }
  182. return sizzle(`error[type="cancel"] service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length > 0;
  183. }
  184. u.merge = function merge (first, second) {
  185. /* Merge the second object into the first one.
  186. */
  187. for (var k in second) {
  188. if (_.isObject(first[k])) {
  189. merge(first[k], second[k]);
  190. } else {
  191. first[k] = second[k];
  192. }
  193. }
  194. };
  195. u.applyUserSettings = function applyUserSettings (context, settings, user_settings) {
  196. /* Configuration settings might be nested objects. We only want to
  197. * add settings which are whitelisted.
  198. */
  199. for (var k in settings) {
  200. if (user_settings[k] === undefined) {
  201. continue;
  202. }
  203. if (_.isObject(settings[k]) && !Array.isArray(settings[k])) {
  204. applyUserSettings(context[k], settings[k], user_settings[k]);
  205. } else {
  206. context[k] = user_settings[k];
  207. }
  208. }
  209. };
  210. /**
  211. * Converts an HTML string into a DOM Node.
  212. * Expects that the HTML string has only one top-level element,
  213. * i.e. not multiple ones.
  214. * @private
  215. * @method u#stringToNode
  216. * @param { String } s - The HTML string
  217. */
  218. u.stringToNode = function (s) {
  219. var div = document.createElement('div');
  220. div.innerHTML = s;
  221. return div.firstElementChild;
  222. };
  223. u.getOuterWidth = function (el, include_margin=false) {
  224. let width = el.offsetWidth;
  225. if (!include_margin) {
  226. return width;
  227. }
  228. const style = window.getComputedStyle(el);
  229. width += parseInt(style.marginLeft ? style.marginLeft : 0, 10) +
  230. parseInt(style.marginRight ? style.marginRight : 0, 10);
  231. return width;
  232. };
  233. /**
  234. * Converts an HTML string into a DOM element.
  235. * Expects that the HTML string has only one top-level element,
  236. * i.e. not multiple ones.
  237. * @private
  238. * @method u#stringToElement
  239. * @param { String } s - The HTML string
  240. */
  241. u.stringToElement = function (s) {
  242. var div = document.createElement('div');
  243. div.innerHTML = s;
  244. return div.firstElementChild;
  245. };
  246. /**
  247. * Checks whether the DOM element matches the given selector.
  248. * @private
  249. * @method u#matchesSelector
  250. * @param { DOMElement } el - The DOM element
  251. * @param { String } selector - The selector
  252. */
  253. u.matchesSelector = function (el, selector) {
  254. const match = (
  255. el.matches ||
  256. el.matchesSelector ||
  257. el.msMatchesSelector ||
  258. el.mozMatchesSelector ||
  259. el.webkitMatchesSelector ||
  260. el.oMatchesSelector
  261. );
  262. return match ? match.call(el, selector) : false;
  263. };
  264. /**
  265. * Returns a list of children of the DOM element that match the selector.
  266. * @private
  267. * @method u#queryChildren
  268. * @param { DOMElement } el - the DOM element
  269. * @param { String } selector - the selector they should be matched against
  270. */
  271. u.queryChildren = function (el, selector) {
  272. return Array.from(el.childNodes).filter(el => u.matchesSelector(el, selector));
  273. };
  274. u.contains = function (attr, query) {
  275. return function (item) {
  276. if (typeof attr === 'object') {
  277. var value = false;
  278. _.forEach(attr, function (a) {
  279. value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
  280. });
  281. return value;
  282. } else if (typeof attr === 'string') {
  283. return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
  284. } else {
  285. throw new TypeError('contains: wrong attribute type. Must be string or array.');
  286. }
  287. };
  288. };
  289. u.isOfType = function (type, item) {
  290. return item.get('type') == type;
  291. };
  292. u.isInstance = function (type, item) {
  293. return item instanceof type;
  294. };
  295. u.getAttribute = function (key, item) {
  296. return item.get(key);
  297. };
  298. u.contains.not = function (attr, query) {
  299. return function (item) {
  300. return !(u.contains(attr, query)(item));
  301. };
  302. };
  303. u.rootContains = function (root, el) {
  304. // The document element does not have the contains method in IE.
  305. if (root === document && !root.contains) {
  306. return document.head.contains(el) || document.body.contains(el);
  307. }
  308. return root.contains ? root.contains(el) : window.HTMLElement.prototype.contains.call(root, el);
  309. };
  310. u.createFragmentFromText = function (markup) {
  311. /* Returns a DocumentFragment containing DOM nodes based on the
  312. * passed-in markup text.
  313. */
  314. // http://stackoverflow.com/questions/9334645/create-node-from-markup-string
  315. var frag = document.createDocumentFragment(),
  316. tmp = document.createElement('body'), child;
  317. tmp.innerHTML = markup;
  318. // Append elements in a loop to a DocumentFragment, so that the
  319. // browser does not re-render the document for each node.
  320. while (child = tmp.firstChild) { // eslint-disable-line no-cond-assign
  321. frag.appendChild(child);
  322. }
  323. return frag
  324. };
  325. u.isPersistableModel = function (model) {
  326. return model.collection && model.collection.browserStorage;
  327. };
  328. u.getResolveablePromise = function () {
  329. /* Returns a promise object on which `resolve` or `reject` can be
  330. * called.
  331. */
  332. const wrapper = {
  333. isResolved: false,
  334. isPending: true,
  335. isRejected: false
  336. };
  337. const promise = new Promise((resolve, reject) => {
  338. wrapper.resolve = resolve;
  339. wrapper.reject = reject;
  340. })
  341. _.assign(promise, wrapper);
  342. promise.then(
  343. function (v) {
  344. promise.isResolved = true;
  345. promise.isPending = false;
  346. promise.isRejected = false;
  347. return v;
  348. },
  349. function (e) {
  350. promise.isResolved = false;
  351. promise.isPending = false;
  352. promise.isRejected = true;
  353. throw (e);
  354. }
  355. );
  356. return promise;
  357. };
  358. u.interpolate = function (string, o) {
  359. return string.replace(/{{{([^{}]*)}}}/g,
  360. (a, b) => {
  361. var r = o[b];
  362. return typeof r === 'string' || typeof r === 'number' ? r : a;
  363. });
  364. };
  365. /**
  366. * Call the callback once all the events have been triggered
  367. * @private
  368. * @method u#onMultipleEvents
  369. * @param { Array } events: An array of objects, with keys `object` and
  370. * `event`, representing the event name and the object it's triggered upon.
  371. * @param { Function } callback: The function to call once all events have
  372. * been triggered.
  373. */
  374. u.onMultipleEvents = function (events=[], callback) {
  375. let triggered = [];
  376. function handler (result) {
  377. triggered.push(result)
  378. if (events.length === triggered.length) {
  379. callback(triggered);
  380. triggered = [];
  381. }
  382. }
  383. events.forEach(e => e.object.on(e.event, handler));
  384. };
  385. u.safeSave = function (model, attributes) {
  386. if (u.isPersistableModel(model)) {
  387. model.save(attributes);
  388. } else {
  389. model.set(attributes);
  390. }
  391. };
  392. u.siblingIndex = function (el) {
  393. /* eslint-disable no-cond-assign */
  394. for (var i = 0; el = el.previousElementSibling; i++);
  395. return i;
  396. };
  397. /**
  398. * Returns the current word being written in the input element
  399. * @method u#getCurrentWord
  400. * @param {HTMLElement} input - The HTMLElement in which text is being entered
  401. * @param {integer} [index] - An optional rightmost boundary index. If given, the text
  402. * value of the input element will only be considered up until this index.
  403. * @param {string} [delineator] - An optional string delineator to
  404. * differentiate between words.
  405. * @private
  406. */
  407. u.getCurrentWord = function (input, index, delineator) {
  408. if (!index) {
  409. index = input.selectionEnd || undefined;
  410. }
  411. let [word] = input.value.slice(0, index).split(' ').slice(-1);
  412. if (delineator) {
  413. [word] = word.split(delineator).slice(-1);
  414. }
  415. return word;
  416. };
  417. u.replaceCurrentWord = function (input, new_value) {
  418. const caret = input.selectionEnd || undefined,
  419. current_word = _.last(input.value.slice(0, caret).split(' ')),
  420. value = input.value;
  421. input.value = value.slice(0, caret - current_word.length) + `${new_value} ` + value.slice(caret);
  422. input.selectionEnd = caret - current_word.length + new_value.length + 1;
  423. };
  424. u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true) {
  425. const evt = document.createEvent(type);
  426. evt.initEvent(name, bubbles, cancelable);
  427. el.dispatchEvent(evt);
  428. };
  429. u.geoUriToHttp = function(text, geouri_replacement) {
  430. const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
  431. return text.replace(regex, geouri_replacement);
  432. };
  433. u.httpToGeoUri = function(text, _converse) {
  434. const replacement = 'geo:$1,$2';
  435. return text.replace(_converse.geouri_regex, replacement);
  436. };
  437. u.getSelectValues = function (select) {
  438. const result = [];
  439. const options = select && select.options;
  440. for (var i=0, iLen=options.length; i<iLen; i++) {
  441. const opt = options[i];
  442. if (opt.selected) {
  443. result.push(opt.value || opt.text);
  444. }
  445. }
  446. return result;
  447. };
  448. u.formatFingerprint = function (fp) {
  449. fp = fp.replace(/^05/, '');
  450. for (let i=1; i<8; i++) {
  451. const idx = i*8+i-1;
  452. fp = fp.slice(0, idx) + ' ' + fp.slice(idx);
  453. }
  454. return fp;
  455. };
  456. u.appendArrayBuffer = function (buffer1, buffer2) {
  457. const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
  458. tmp.set(new Uint8Array(buffer1), 0);
  459. tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
  460. return tmp.buffer;
  461. };
  462. u.arrayBufferToHex = function (ab) {
  463. // https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979
  464. return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
  465. };
  466. u.arrayBufferToString = function (ab) {
  467. return new TextDecoder("utf-8").decode(ab);
  468. };
  469. u.stringToArrayBuffer = function (string) {
  470. const bytes = new TextEncoder("utf-8").encode(string);
  471. return bytes.buffer;
  472. };
  473. u.arrayBufferToBase64 = function (ab) {
  474. return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''));
  475. };
  476. u.base64ToArrayBuffer = function (b64) {
  477. const binary_string = window.atob(b64),
  478. len = binary_string.length,
  479. bytes = new Uint8Array(len);
  480. for (let i = 0; i < len; i++) {
  481. bytes[i] = binary_string.charCodeAt(i)
  482. }
  483. return bytes.buffer
  484. };
  485. u.getRandomInt = function (max) {
  486. return Math.floor(Math.random() * Math.floor(max));
  487. };
  488. u.placeCaretAtEnd = function (textarea) {
  489. if (textarea !== document.activeElement) {
  490. textarea.focus();
  491. }
  492. // Double the length because Opera is inconsistent about whether a carriage return is one character or two.
  493. const len = textarea.value.length * 2;
  494. // Timeout seems to be required for Blink
  495. setTimeout(() => textarea.setSelectionRange(len, len), 1);
  496. // Scroll to the bottom, in case we're in a tall textarea
  497. // (Necessary for Firefox and Chrome)
  498. this.scrollTop = 999999;
  499. };
  500. u.getUniqueId = function () {
  501. return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) {
  502. var r = Math.random() * 16 | 0,
  503. v = c === 'x' ? r : r & 0x3 | 0x8;
  504. return v.toString(16);
  505. });
  506. };
  507. /**
  508. * Clears the specified timeout and interval.
  509. * @method u#clearTimers
  510. * @param {number} timeout - Id if the timeout to clear.
  511. * @param {number} interval - Id of the interval to clear.
  512. * @private
  513. * @copyright Simen Bekkhus 2016
  514. * @license MIT
  515. */
  516. function clearTimers(timeout, interval) {
  517. clearTimeout(timeout);
  518. clearInterval(interval);
  519. }
  520. /**
  521. * Creates a {@link Promise} that resolves if the passed in function returns a truthy value.
  522. * Rejects if it throws or does not return truthy within the given max_wait.
  523. * @method u#waitUntil
  524. * @param {Function} func - The function called every check_delay,
  525. * and the result of which is the resolved value of the promise.
  526. * @param {number} [max_wait=300] - The time to wait before rejecting the promise.
  527. * @param {number} [check_delay=3] - The time to wait before each invocation of {func}.
  528. * @returns {Promise} A promise resolved with the value of func,
  529. * or rejected with the exception thrown by it or it times out.
  530. * @copyright Simen Bekkhus 2016
  531. * @license MIT
  532. */
  533. u.waitUntil = function (func, max_wait=300, check_delay=3) {
  534. // Run the function once without setting up any listeners in case it's already true
  535. try {
  536. const result = func();
  537. if (result) {
  538. return Promise.resolve(result);
  539. }
  540. } catch (e) {
  541. return Promise.reject(e);
  542. }
  543. const promise = u.getResolveablePromise();
  544. function checker () {
  545. try {
  546. const result = func();
  547. if (result) {
  548. clearTimers(max_wait_timeout, interval);
  549. promise.resolve(result);
  550. }
  551. } catch (e) {
  552. clearTimers(max_wait_timeout, interval);
  553. promise.reject(e);
  554. }
  555. }
  556. const interval = setInterval(checker, check_delay);
  557. const max_wait_timeout = setTimeout(() => {
  558. clearTimers(max_wait_timeout, interval);
  559. const err_msg = 'Wait until promise timed out';
  560. u.logger.error(err_msg);
  561. promise.reject(new Error(err_msg));
  562. }, max_wait);
  563. return promise;
  564. };
  565. export default u;