Stackedit/src/services/utils.js
2017-09-26 23:54:26 +01:00

358 lines
11 KiB
JavaScript

import yaml from 'js-yaml';
import defaultProperties from '../data/defaultFileProperties.yml';
const workspaceId = 'main';
const origin = `${location.protocol}//${location.host}`;
// For uid()
const uidLength = 16;
const crypto = window.crypto || window.msCrypto;
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const radix = alphabet.length;
const array = new Uint32Array(uidLength);
// For isUserActive
const inactiveAfter = 2 * 60 * 1000; // 2 minutes
let lastActivity;
const setLastActivity = () => {
lastActivity = Date.now();
};
window.document.addEventListener('mousedown', setLastActivity);
window.document.addEventListener('keydown', setLastActivity);
window.document.addEventListener('touchstart', setLastActivity);
// For isWindowFocused
let lastFocus;
const lastFocusKey = `${workspaceId}/lastWindowFocus`;
const setLastFocus = () => {
lastFocus = Date.now();
localStorage[lastFocusKey] = lastFocus;
setLastActivity();
};
setLastFocus();
window.addEventListener('focus', setLastFocus);
// For addQueryParams()
const urlParser = window.document.createElement('a');
// For loadScript()
const scriptLoadingPromises = Object.create(null);
// For startOauth2
const oauth2AuthorizationTimeout = 120 * 1000; // 2 minutes
// For checkOnline
const checkOnlineTimeout = 15 * 1000; // 15 sec
export default {
workspaceId,
origin,
oauth2RedirectUri: `${origin}/oauth2/callback`,
types: [
'contentState',
'syncedContent',
'content',
'file',
'folder',
'syncLocation',
'publishLocation',
'data',
],
deepCopy(obj) {
return obj == null ? obj : JSON.parse(JSON.stringify(obj));
},
serializeObject(obj) {
return obj === undefined ? obj : JSON.stringify(obj, (key, value) => {
if (Object.prototype.toString.call(value) !== '[object Object]') {
return value;
}
// Sort keys to have a predictable result
return Object.keys(value).sort().reduce((sorted, valueKey) => {
sorted[valueKey] = value[valueKey];
return sorted;
}, {});
});
},
uid() {
crypto.getRandomValues(array);
return array.cl_map(value => alphabet[value % radix]).join('');
},
hash(str) {
let hash = 0;
if (!str) return hash;
for (let i = 0; i < str.length; i += 1) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char; // eslint-disable-line no-bitwise
hash |= 0; // eslint-disable-line no-bitwise
}
return hash;
},
encodeBase64(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
(match, p1) => String.fromCharCode(`0x${p1}`)));
},
decodeBase64(str) {
return decodeURIComponent(atob(str).split('').map(
c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`).join(''));
},
computeProperties(yamlProperties) {
const customProperties = yaml.safeLoad(yamlProperties);
const properties = yaml.safeLoad(defaultProperties);
const override = (obj, opt) => {
const objType = Object.prototype.toString.call(obj);
const optType = Object.prototype.toString.call(opt);
if (objType !== optType) {
return obj;
} else if (objType !== '[object Object]') {
return opt === undefined ? obj : opt;
}
Object.keys({
...obj,
...opt,
}).forEach((key) => {
obj[key] = override(obj[key], opt[key]);
});
return obj;
};
return override(properties, customProperties);
},
randomize(value) {
return Math.floor((1 + (Math.random() * 0.2)) * value);
},
setInterval(func, interval) {
return setInterval(() => func(), this.randomize(interval));
},
isWindowFocused() {
return parseInt(localStorage[lastFocusKey], 10) === lastFocus;
},
isUserActive() {
return lastActivity > Date.now() - inactiveAfter && this.isWindowFocused();
},
addQueryParams(url = '', params = {}) {
const keys = Object.keys(params).filter(key => params[key] != null);
if (!keys.length) {
return url;
}
urlParser.href = url;
if (urlParser.search) {
urlParser.search += '&';
} else {
urlParser.search = '?';
}
urlParser.search += keys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
return urlParser.href;
},
loadScript(url) {
if (!scriptLoadingPromises[url]) {
scriptLoadingPromises[url] = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.onload = resolve;
script.onerror = () => {
scriptLoadingPromises[url] = null;
reject();
};
script.src = url;
document.head.appendChild(script);
});
}
return scriptLoadingPromises[url];
},
startOauth2(url, params = {}, silent = false) {
const oauth2Context = {};
// Build the authorize URL
const state = this.uid();
params.state = state;
params.redirect_uri = this.oauth2RedirectUri;
const authorizeUrl = this.addQueryParams(url, params);
if (silent) {
// Use an iframe as wnd for silent mode
oauth2Context.iframeElt = document.createElement('iframe');
oauth2Context.iframeElt.style.position = 'absolute';
oauth2Context.iframeElt.style.left = '-9999px';
oauth2Context.closeTimeout = setTimeout(
() => oauth2Context.clean('Unknown error.'),
checkOnlineTimeout);
oauth2Context.iframeElt.onerror = () => oauth2Context.clean('Unknown error.');
oauth2Context.iframeElt.src = authorizeUrl;
document.body.appendChild(oauth2Context.iframeElt);
oauth2Context.wnd = oauth2Context.iframeElt.contentWindow;
} else {
// Open a new tab otherwise
oauth2Context.wnd = window.open(authorizeUrl);
// If window opening has been blocked by the browser
if (!oauth2Context.wnd) {
return Promise.reject();
}
oauth2Context.closeTimeout = setTimeout(
() => oauth2Context.clean('Timeout.'),
oauth2AuthorizationTimeout);
}
return new Promise((resolve, reject) => {
oauth2Context.clean = (errorMsg) => {
clearInterval(oauth2Context.checkClosedInterval);
if (!silent && !oauth2Context.wnd.closed) {
oauth2Context.wnd.close();
}
if (oauth2Context.iframeElt) {
document.body.removeChild(oauth2Context.iframeElt);
}
clearTimeout(oauth2Context.closeTimeout);
window.removeEventListener('message', oauth2Context.msgHandler);
oauth2Context.clean = () => {
// Prevent from cleaning several times
};
if (errorMsg) {
reject(new Error(errorMsg));
}
};
oauth2Context.msgHandler = (event) => {
if (event.source === oauth2Context.wnd &&
event.origin === this.origin
) {
oauth2Context.clean();
const data = {};
`${event.data}`.slice(1).split('&').forEach((param) => {
const [key, value] = param.split('=').map(decodeURIComponent);
if (key === 'state') {
data.state = value;
} else if (key === 'access_token') {
data.accessToken = value;
} else if (key === 'code') {
data.code = value;
} else if (key === 'expires_in') {
data.expiresIn = value;
}
});
if (data.state === state) {
resolve(data);
return;
}
reject('Could not get required authorization.');
}
};
window.addEventListener('message', oauth2Context.msgHandler);
oauth2Context.checkClosedInterval = !silent && setInterval(() => {
if (oauth2Context.wnd.closed) {
oauth2Context.clean('Authorize window was closed.');
}
}, 250);
});
},
request(configParam) {
let retryAfter = 500; // 500 ms
const maxRetryAfter = 30 * 1000; // 30 sec
const config = Object.assign({}, configParam);
config.timeout = config.timeout || 30 * 1000; // 30 sec
config.headers = Object.assign({}, config.headers);
if (config.body && typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
config.headers['Content-Type'] = 'application/json';
}
function parseHeaders(xhr) {
const pairs = xhr.getAllResponseHeaders().trim().split('\n');
return pairs.reduce((headers, header) => {
const split = header.trim().split(':');
const key = split.shift().trim().toLowerCase();
const value = split.join(':').trim();
headers[key] = value;
return headers;
}, {});
}
function isRetriable(err) {
switch (err.status) {
case 403:
{
const googleReason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
return googleReason === 'rateLimitExceeded' || googleReason === 'userRateLimitExceeded';
}
case 429:
return true;
default:
if (err.status >= 500 && err.status < 600) {
return true;
}
}
return false;
}
const attempt =
() => new Promise((resolve, reject) => {
const xhr = new window.XMLHttpRequest();
let timeoutId;
xhr.onload = () => {
clearTimeout(timeoutId);
const result = {
status: xhr.status,
headers: parseHeaders(xhr),
body: xhr.responseText,
};
if (!config.raw) {
try {
result.body = JSON.parse(result.body);
} catch (e) {
// ignore
}
}
if (result.status >= 200 && result.status < 300) {
resolve(result);
return;
}
reject(result);
};
xhr.onerror = () => {
clearTimeout(timeoutId);
reject(new Error('Network request failed.'));
};
timeoutId = setTimeout(() => {
xhr.abort();
reject(new Error('Network request timeout.'));
}, config.timeout);
const url = this.addQueryParams(config.url, config.params);
xhr.open(config.method || 'GET', url);
Object.keys(config.headers).forEach((key) => {
const value = config.headers[key];
if (value) {
xhr.setRequestHeader(key, `${value}`);
}
});
xhr.send(config.body || null);
})
.catch((err) => {
// Try again later in case of retriable error
if (isRetriable(err) && retryAfter < maxRetryAfter) {
return new Promise(
(resolve) => {
setTimeout(resolve, retryAfter);
// Exponential backoff
retryAfter *= 2;
})
.then(attempt);
}
throw err;
});
return attempt();
},
checkOnline() {
const checkStatus = (res) => {
if (!res.status || res.status < 200) {
throw new Error('Offline...');
}
};
return this.request({
url: 'https://www.googleapis.com/plus/v1/people/me',
timeout: checkOnlineTimeout,
})
.then(checkStatus, checkStatus);
},
};