invidious/assets/js/_helpers.js

223 lines
7.8 KiB
JavaScript
Raw Normal View History

'use strict';
// Contains only auxiliary methods
// May be included and executed unlimited number of times without any consequences
// Polyfills for IE11
Array.prototype.find = Array.prototype.find || function (condition) {
return this.filter(condition)[0];
};
Array.from = Array.from || function (source) {
return Array.prototype.slice.call(source);
};
NodeList.prototype.forEach = NodeList.prototype.forEach || function (callback) {
Array.from(this).forEach(callback);
};
String.prototype.includes = String.prototype.includes || function (searchString) {
return this.indexOf(searchString) >= 0;
};
String.prototype.startsWith = String.prototype.startsWith || function (prefix) {
return this.substr(0, prefix.length) === prefix;
};
Math.sign = Math.sign || function(x) {
x = +x;
if (!x) return x; // 0 and NaN
return x > 0 ? 1 : -1;
};
// Monstrous global variable for handy code
2022-05-05 20:16:41 -07:00
window.helpers = window.helpers || {
/**
* https://en.wikipedia.org/wiki/Clamping_(graphics)
* @param {Number} num Source number
* @param {Number} min Low border
* @param {Number} max High border
* @returns {Number} Clamped value
*/
clamp: function (num, min, max) {
if (max < min) {
var t = max; max = min; min = t; // swap max and min
}
2022-05-05 20:16:41 -07:00
if (max < num)
return max;
2022-05-05 20:16:41 -07:00
if (min > num)
return min;
return num;
},
/** @private */
_xhr: function (method, url, options, callbacks) {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
// Default options
xhr.responseType = 'json';
xhr.timeout = 10000;
// Default options redefining
if (options.responseType)
xhr.responseType = options.responseType;
if (options.timeout)
xhr.timeout = options.timeout;
if (method === 'POST')
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
2022-05-05 20:16:41 -07:00
// better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963
xhr.onloadend = function () {
if (xhr.status === 200) {
if (callbacks.on200)
callbacks.on200(xhr.response);
} else {
// handled by onerror
if (xhr.status === 0) return;
if (callbacks.onNon200)
callbacks.onNon200(xhr);
}
};
xhr.ontimeout = function () {
if (callbacks.onTimeout)
callbacks.onTimeout(xhr);
};
xhr.onerror = function () {
if (callbacks.onError)
callbacks.onError(xhr);
};
if (options.payload)
xhr.send(options.payload);
else
xhr.send();
},
/** @private */
2022-05-05 20:16:41 -07:00
_xhrRetry: function(method, url, options, callbacks) {
if (options.retries <= 0) {
console.warn('Failed to pull', options.entity_name);
if (callbacks.onTotalFail)
callbacks.onTotalFail();
return;
}
2022-05-05 20:16:41 -07:00
helpers._xhr(method, url, options, callbacks);
},
/**
* @callback callbackXhrOn200
* @param {Object} response - xhr.response
*/
/**
* @callback callbackXhrError
* @param {XMLHttpRequest} xhr
*/
/**
* @param {'GET'|'POST'} method - 'GET' or 'POST'
* @param {String} url - URL to send request to
* @param {Object} options - other XHR options
* @param {XMLHttpRequestBodyInit} [options.payload=null] - payload for POST-requests
* @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [options.responseType=json]
* @param {Number} [options.timeout=10000]
* @param {Number} [options.retries=1]
* @param {String} [options.entity_name='unknown'] - string to log
* @param {Number} [options.retry_timeout=1000]
* @param {Object} callbacks - functions to execute on events fired
* @param {callbackXhrOn200} [callbacks.on200]
* @param {callbackXhrError} [callbacks.onNon200]
* @param {callbackXhrError} [callbacks.onTimeout]
* @param {callbackXhrError} [callbacks.onError]
* @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries
*/
2022-05-05 20:16:41 -07:00
xhr: function(method, url, options, callbacks) {
if (!options.retries || options.retries <= 1) {
helpers._xhr(method, url, options, callbacks);
return;
}
if (!options.entity_name) options.entity_name = 'unknown';
2022-05-05 20:16:41 -07:00
if (!options.retry_timeout) options.retry_timeout = 1000;
const retries_total = options.retries;
2022-05-05 20:16:41 -07:00
let currentTry = 1;
const retry = function () {
2022-05-05 20:16:41 -07:00
console.warn('Pulling ' + options.entity_name + ' failed... ' + (currentTry++) + '/' + retries_total);
setTimeout(function () {
options.retries--;
helpers._xhrRetry(method, url, options, callbacks);
}, options.retry_timeout);
};
if (callbacks.onError)
callbacks._onError = callbacks.onError;
callbacks.onError = function (xhr) {
if (callbacks._onError)
callbacks._onError();
retry();
};
if (callbacks.onTimeout)
callbacks._onTimeout = callbacks.onTimeout;
callbacks.onTimeout = function (xhr) {
if (callbacks._onTimeout)
callbacks._onTimeout();
retry();
};
helpers._xhrRetry(method, url, options, callbacks);
},
/**
* @typedef {Object} invidiousStorage
* @property {(key:String) => Object|null} get
* @property {(key:String, value:Object) => null} set
* @property {(key:String) => null} remove
*/
/**
* Universal storage proxy. Uses inside localStorage or cookies
* @type {invidiousStorage}
*/
storage: (function () {
// access to localStorage throws exception in Tor Browser, so try is needed
let localStorageIsUsable = false;
try{localStorageIsUsable = !!localStorage.setItem;}catch(e){}
if (localStorageIsUsable) {
return {
get: function (key) { return localStorage[key]; },
set: function (key, value) { localStorage[key] = value; },
remove: function (key) { localStorage.removeItem(key); }
};
}
console.info('Storage: localStorage is disabled or unaccessible trying cookies');
return {
get: function (key) {
const cookiePrefix = key + '=';
function findCallback(cookie) {return cookie.startsWith(cookiePrefix);}
const matchedCookie = document.cookie.split(';').find(findCallback);
if (matchedCookie)
return matchedCookie.replace(cookiePrefix, '');
return null;
},
set: function (key, value) {
const cookie_data = encodeURIComponent(JSON.stringify(value));
// Set expiration in 2 year
const date = new Date();
date.setTime(date.getTime() + 2*365.25*24*60*60);
const ip_regex = /^((\d+\.){3}\d+|[A-Fa-f0-9]*:[A-Fa-f0-9:]*:[A-Fa-f0-9:]+)$/;
let domain_used = location.hostname;
// Fix for a bug in FF where the leading dot in the FQDN is not ignored
if (domain_used.charAt(0) !== '.' && !ip_regex.test(domain_used) && domain_used !== 'localhost')
domain_used = '.' + location.hostname;
document.cookie = key + '=' + cookie_data + '; SameSite=Strict; path=/; domain=' +
domain_used + '; expires=' + date.toGMTString() + ';';
},
remove: function (key) {
document.cookie = key + '=; Max-Age=0';
}
};
})()
};