_helpers.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. /* @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0 */
  2. 'use strict';
  3. // Contains only auxiliary methods
  4. // May be included and executed unlimited number of times without any consequences
  5. // Polyfills for IE11
  6. Array.prototype.find = Array.prototype.find || function (condition) {
  7. return this.filter(condition)[0];
  8. };
  9. Array.from = Array.from || function (source) {
  10. return Array.prototype.slice.call(source);
  11. };
  12. NodeList.prototype.forEach = NodeList.prototype.forEach || function (callback) {
  13. Array.from(this).forEach(callback);
  14. };
  15. String.prototype.includes = String.prototype.includes || function (searchString) {
  16. return this.indexOf(searchString) >= 0;
  17. };
  18. String.prototype.startsWith = String.prototype.startsWith || function (prefix) {
  19. return this.substr(0, prefix.length) === prefix;
  20. };
  21. Math.sign = Math.sign || function(x) {
  22. x = +x;
  23. if (!x) return x; // 0 and NaN
  24. return x > 0 ? 1 : -1;
  25. };
  26. if (!window.hasOwnProperty('HTMLDetailsElement') && !window.hasOwnProperty('mockHTMLDetailsElement')) {
  27. window.mockHTMLDetailsElement = true;
  28. const style = 'details:not([open]) > :not(summary) {display: none}';
  29. document.head.appendChild(document.createElement('style')).textContent = style;
  30. addEventListener('click', function (e) {
  31. if (e.target.nodeName !== 'SUMMARY') return;
  32. const details = e.target.parentElement;
  33. if (details.hasAttribute('open'))
  34. details.removeAttribute('open');
  35. else
  36. details.setAttribute('open', '');
  37. });
  38. }
  39. // Monstrous global variable for handy code
  40. // Includes: clamp, xhr, storage.{get,set,remove}
  41. window.helpers = window.helpers || {
  42. /**
  43. * https://en.wikipedia.org/wiki/Clamping_(graphics)
  44. * @param {Number} num Source number
  45. * @param {Number} min Low border
  46. * @param {Number} max High border
  47. * @returns {Number} Clamped value
  48. */
  49. clamp: function (num, min, max) {
  50. if (max < min) {
  51. var t = max; max = min; min = t; // swap max and min
  52. }
  53. if (max < num)
  54. return max;
  55. if (min > num)
  56. return min;
  57. return num;
  58. },
  59. /** @private */
  60. _xhr: function (method, url, options, callbacks) {
  61. const xhr = new XMLHttpRequest();
  62. xhr.open(method, url);
  63. // Default options
  64. xhr.responseType = 'json';
  65. xhr.timeout = 10000;
  66. // Default options redefining
  67. if (options.responseType)
  68. xhr.responseType = options.responseType;
  69. if (options.timeout)
  70. xhr.timeout = options.timeout;
  71. if (method === 'POST')
  72. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  73. // better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963
  74. xhr.onloadend = function () {
  75. if (xhr.status === 200) {
  76. if (callbacks.on200) {
  77. // fix for IE11. It doesn't convert response to JSON
  78. if (xhr.responseType === '' && typeof(xhr.response) === 'string')
  79. callbacks.on200(JSON.parse(xhr.response));
  80. else
  81. callbacks.on200(xhr.response);
  82. }
  83. } else {
  84. // handled by onerror
  85. if (xhr.status === 0) return;
  86. if (callbacks.onNon200)
  87. callbacks.onNon200(xhr);
  88. }
  89. };
  90. xhr.ontimeout = function () {
  91. if (callbacks.onTimeout)
  92. callbacks.onTimeout(xhr);
  93. };
  94. xhr.onerror = function () {
  95. if (callbacks.onError)
  96. callbacks.onError(xhr);
  97. };
  98. if (options.payload)
  99. xhr.send(options.payload);
  100. else
  101. xhr.send();
  102. },
  103. /** @private */
  104. _xhrRetry: function(method, url, options, callbacks) {
  105. if (options.retries <= 0) {
  106. console.warn('Failed to pull', options.entity_name);
  107. if (callbacks.onTotalFail)
  108. callbacks.onTotalFail();
  109. return;
  110. }
  111. helpers._xhr(method, url, options, callbacks);
  112. },
  113. /**
  114. * @callback callbackXhrOn200
  115. * @param {Object} response - xhr.response
  116. */
  117. /**
  118. * @callback callbackXhrError
  119. * @param {XMLHttpRequest} xhr
  120. */
  121. /**
  122. * @param {'GET'|'POST'} method - 'GET' or 'POST'
  123. * @param {String} url - URL to send request to
  124. * @param {Object} options - other XHR options
  125. * @param {XMLHttpRequestBodyInit} [options.payload=null] - payload for POST-requests
  126. * @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [options.responseType=json]
  127. * @param {Number} [options.timeout=10000]
  128. * @param {Number} [options.retries=1]
  129. * @param {String} [options.entity_name='unknown'] - string to log
  130. * @param {Number} [options.retry_timeout=1000]
  131. * @param {Object} callbacks - functions to execute on events fired
  132. * @param {callbackXhrOn200} [callbacks.on200]
  133. * @param {callbackXhrError} [callbacks.onNon200]
  134. * @param {callbackXhrError} [callbacks.onTimeout]
  135. * @param {callbackXhrError} [callbacks.onError]
  136. * @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries
  137. */
  138. xhr: function(method, url, options, callbacks) {
  139. if (!options.retries || options.retries <= 1) {
  140. helpers._xhr(method, url, options, callbacks);
  141. return;
  142. }
  143. if (!options.entity_name) options.entity_name = 'unknown';
  144. if (!options.retry_timeout) options.retry_timeout = 1000;
  145. const retries_total = options.retries;
  146. let currentTry = 1;
  147. const retry = function () {
  148. console.warn('Pulling ' + options.entity_name + ' failed... ' + (currentTry++) + '/' + retries_total);
  149. setTimeout(function () {
  150. options.retries--;
  151. helpers._xhrRetry(method, url, options, callbacks);
  152. }, options.retry_timeout);
  153. };
  154. // Pack retry() call into error handlers
  155. callbacks._onError = callbacks.onError;
  156. callbacks.onError = function (xhr) {
  157. if (callbacks._onError)
  158. callbacks._onError(xhr);
  159. retry();
  160. };
  161. callbacks._onTimeout = callbacks.onTimeout;
  162. callbacks.onTimeout = function (xhr) {
  163. if (callbacks._onTimeout)
  164. callbacks._onTimeout(xhr);
  165. retry();
  166. };
  167. helpers._xhrRetry(method, url, options, callbacks);
  168. },
  169. /**
  170. * @typedef {Object} invidiousStorage
  171. * @property {(key:String) => Object} get
  172. * @property {(key:String, value:Object)} set
  173. * @property {(key:String)} remove
  174. */
  175. /**
  176. * Universal storage, stores and returns JS objects. Uses inside localStorage or cookies
  177. * @type {invidiousStorage}
  178. */
  179. storage: (function () {
  180. // access to localStorage throws exception in Tor Browser, so try is needed
  181. let localStorageIsUsable = false;
  182. try{localStorageIsUsable = !!localStorage.setItem;}catch(e){}
  183. if (localStorageIsUsable) {
  184. return {
  185. get: function (key) {
  186. if (!localStorage[key]) return;
  187. try {
  188. return JSON.parse(decodeURIComponent(localStorage[key]));
  189. } catch(e) {
  190. // Erase non parsable value
  191. helpers.storage.remove(key);
  192. }
  193. },
  194. set: function (key, value) { localStorage[key] = encodeURIComponent(JSON.stringify(value)); },
  195. remove: function (key) { localStorage.removeItem(key); }
  196. };
  197. }
  198. // TODO: fire 'storage' event for cookies
  199. console.info('Storage: localStorage is disabled or unaccessible. Cookies used as fallback');
  200. return {
  201. get: function (key) {
  202. const cookiePrefix = key + '=';
  203. function findCallback(cookie) {return cookie.startsWith(cookiePrefix);}
  204. const matchedCookie = document.cookie.split('; ').find(findCallback);
  205. if (matchedCookie) {
  206. const cookieBody = matchedCookie.replace(cookiePrefix, '');
  207. if (cookieBody.length === 0) return;
  208. try {
  209. return JSON.parse(decodeURIComponent(cookieBody));
  210. } catch(e) {
  211. // Erase non parsable value
  212. helpers.storage.remove(key);
  213. }
  214. }
  215. },
  216. set: function (key, value) {
  217. const cookie_data = encodeURIComponent(JSON.stringify(value));
  218. // Set expiration in 2 year
  219. const date = new Date();
  220. date.setFullYear(date.getFullYear()+2);
  221. document.cookie = key + '=' + cookie_data + '; expires=' + date.toGMTString();
  222. },
  223. remove: function (key) {
  224. document.cookie = key + '=; Max-Age=0';
  225. }
  226. };
  227. })()
  228. };
  229. /* @license-end */