2017-07-12 18:17:04 +00:00
|
|
|
'use strict';
|
|
|
|
|
2017-11-08 03:46:56 +00:00
|
|
|
const CHROMIUM = chrome.app && /Chromium/.test(navigator.userAgent); // non-Windows Chromium
|
|
|
|
const FIREFOX = !chrome.app;
|
|
|
|
const VIVALDI = chrome.app && /Vivaldi/.test(navigator.userAgent);
|
|
|
|
const OPERA = chrome.app && /OPR/.test(navigator.userAgent);
|
2017-07-12 18:17:04 +00:00
|
|
|
|
2017-11-08 03:52:51 +00:00
|
|
|
document.addEventListener('stylishUpdate', onClick);
|
|
|
|
document.addEventListener('stylishUpdateChrome', onClick);
|
|
|
|
document.addEventListener('stylishUpdateOpera', onClick);
|
2017-07-12 18:17:04 +00:00
|
|
|
|
2017-11-08 03:52:51 +00:00
|
|
|
document.addEventListener('stylishInstall', onClick);
|
|
|
|
document.addEventListener('stylishInstallChrome', onClick);
|
|
|
|
document.addEventListener('stylishInstallOpera', onClick);
|
2017-07-12 18:17:04 +00:00
|
|
|
|
|
|
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
|
|
// orphaned content script check
|
2017-07-16 18:02:00 +00:00
|
|
|
if (msg.method === 'ping') {
|
2017-07-12 18:17:04 +00:00
|
|
|
sendResponse(true);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// TODO: remove the following statement when USO is fixed
|
|
|
|
document.documentElement.appendChild(document.createElement('script')).text = '(' +
|
2017-07-16 16:49:31 +00:00
|
|
|
function () {
|
2017-07-12 18:17:04 +00:00
|
|
|
let settings;
|
2017-11-21 10:42:51 +00:00
|
|
|
const originalResponseJson = Response.prototype.json;
|
2017-07-12 18:17:04 +00:00
|
|
|
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
|
|
|
|
document.removeEventListener('stylusFixBuggyUSOsettings', _);
|
|
|
|
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search);
|
2017-11-21 10:42:51 +00:00
|
|
|
if (!settings) {
|
|
|
|
Response.prototype.json = originalResponseJson;
|
|
|
|
}
|
2017-07-12 18:17:04 +00:00
|
|
|
});
|
2017-07-16 16:49:31 +00:00
|
|
|
Response.prototype.json = function (...args) {
|
2017-07-12 18:17:04 +00:00
|
|
|
return originalResponseJson.call(this, ...args).then(json => {
|
2017-07-16 18:02:00 +00:00
|
|
|
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') {
|
2017-07-12 18:17:04 +00:00
|
|
|
return json;
|
|
|
|
}
|
2017-11-21 10:42:51 +00:00
|
|
|
Response.prototype.json = originalResponseJson;
|
2017-07-12 18:17:04 +00:00
|
|
|
const images = new Map();
|
|
|
|
for (const jsonSetting of json.style_settings) {
|
|
|
|
let value = settings.get('ik-' + jsonSetting.install_key);
|
|
|
|
if (!value
|
|
|
|
|| !jsonSetting.style_setting_options
|
|
|
|
|| !jsonSetting.style_setting_options[0]) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (value.startsWith('ik-')) {
|
|
|
|
value = value.replace(/^ik-/, '');
|
|
|
|
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
|
2017-07-16 18:02:00 +00:00
|
|
|
if (!defaultItem || defaultItem.install_key !== value) {
|
2017-07-12 18:17:04 +00:00
|
|
|
if (defaultItem) {
|
|
|
|
defaultItem.default = false;
|
|
|
|
}
|
|
|
|
jsonSetting.style_setting_options.some(item => {
|
2017-07-16 18:02:00 +00:00
|
|
|
if (item.install_key === value) {
|
2017-07-12 18:17:04 +00:00
|
|
|
item.default = true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2017-07-16 18:02:00 +00:00
|
|
|
} else if (jsonSetting.setting_type === 'image') {
|
2017-07-12 18:17:04 +00:00
|
|
|
jsonSetting.style_setting_options.some(item => {
|
|
|
|
if (item.default) {
|
|
|
|
item.default = false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
images.set(jsonSetting.install_key, value);
|
|
|
|
} else {
|
|
|
|
const item = jsonSetting.style_setting_options[0];
|
2017-07-16 18:02:00 +00:00
|
|
|
if (item.value !== value && item.install_key === 'placeholder') {
|
2017-07-12 18:17:04 +00:00
|
|
|
item.value = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (images.size) {
|
|
|
|
new MutationObserver((_, observer) => {
|
|
|
|
if (!document.getElementById('style-settings')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
observer.disconnect();
|
|
|
|
for (const [name, url] of images.entries()) {
|
|
|
|
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
|
|
|
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
|
|
|
if (elUrl) {
|
|
|
|
elUrl.value = url;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}).observe(document, {childList: true, subtree: true});
|
|
|
|
}
|
|
|
|
return json;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
} + ')()';
|
|
|
|
|
|
|
|
// TODO: remove the following statement when USO pagination is fixed
|
|
|
|
if (location.search.includes('category=')) {
|
|
|
|
document.addEventListener('DOMContentLoaded', function _() {
|
|
|
|
document.removeEventListener('DOMContentLoaded', _);
|
|
|
|
new MutationObserver((_, observer) => {
|
|
|
|
if (!document.getElementById('pagination')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
observer.disconnect();
|
|
|
|
const category = '&' + location.search.match(/category=[^&]+/)[0];
|
|
|
|
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
|
|
|
|
for (let i = 0; i < links.length; i++) {
|
|
|
|
links[i].href += category;
|
|
|
|
}
|
|
|
|
}).observe(document, {childList: true, subtree: true});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
new MutationObserver((mutations, observer) => {
|
|
|
|
if (document.body) {
|
|
|
|
observer.disconnect();
|
|
|
|
// TODO: remove the following statement when USO pagination title is fixed
|
2017-09-30 00:13:59 +00:00
|
|
|
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
|
2017-07-12 18:17:04 +00:00
|
|
|
chrome.runtime.sendMessage({
|
|
|
|
method: 'getStyles',
|
|
|
|
url: getMeta('stylish-id-url') || location.href
|
|
|
|
}, checkUpdatability);
|
|
|
|
}
|
|
|
|
}).observe(document.documentElement, {childList: true});
|
|
|
|
|
|
|
|
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
|
|
|
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
|
|
|
we need to fix this URL using "stylish-update-url" meta key
|
|
|
|
*/
|
|
|
|
function getStyleURL() {
|
2017-11-21 10:23:32 +00:00
|
|
|
const textUrl = getMeta('stylish-update-url') || '';
|
|
|
|
const jsonUrl = getMeta('stylish-code-chrome') ||
|
|
|
|
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
|
|
|
|
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
|
|
|
|
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
2017-07-12 18:17:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function checkUpdatability([installedStyle]) {
|
|
|
|
// TODO: remove the following statement when USO is fixed
|
|
|
|
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
|
|
|
detail: installedStyle && installedStyle.updateUrl,
|
|
|
|
}));
|
|
|
|
if (!installedStyle) {
|
|
|
|
sendEvent('styleCanBeInstalledChrome');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const md5Url = getMeta('stylish-md5-url');
|
|
|
|
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
|
|
|
getResource(md5Url).then(md5 => {
|
2017-07-16 18:02:00 +00:00
|
|
|
reportUpdatable(md5 !== installedStyle.originalMd5);
|
2017-07-12 18:17:04 +00:00
|
|
|
});
|
|
|
|
} else {
|
2017-11-21 10:23:32 +00:00
|
|
|
getStyleJson().then(json => {
|
|
|
|
reportUpdatable(!json ||
|
|
|
|
!styleSectionsEqual(json, installedStyle));
|
2017-07-12 18:17:04 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function reportUpdatable(isUpdatable) {
|
|
|
|
sendEvent(
|
|
|
|
isUpdatable
|
|
|
|
? 'styleCanBeUpdatedChrome'
|
|
|
|
: 'styleAlreadyInstalledChrome',
|
|
|
|
{
|
|
|
|
updateUrl: installedStyle.updateUrl
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sendEvent(type, detail = null) {
|
|
|
|
if (FIREFOX) {
|
|
|
|
type = type.replace('Chrome', '');
|
|
|
|
} else if (OPERA || VIVALDI) {
|
|
|
|
type = type.replace('Chrome', 'Opera');
|
|
|
|
}
|
|
|
|
detail = {detail};
|
2017-07-16 18:02:00 +00:00
|
|
|
if (typeof cloneInto !== 'undefined') {
|
2017-07-12 18:17:04 +00:00
|
|
|
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
|
|
|
// because USO tries to use a global "event" variable deprecated in Firefox
|
|
|
|
detail = cloneInto(detail, document); // eslint-disable-line no-undef
|
|
|
|
}
|
|
|
|
onDOMready().then(() => {
|
|
|
|
document.dispatchEvent(new CustomEvent(type, detail));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-08 03:52:51 +00:00
|
|
|
function onClick(event) {
|
|
|
|
if (onClick.processing || !orphanCheck || !orphanCheck()) {
|
2017-07-12 18:17:04 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-11-08 03:52:51 +00:00
|
|
|
onClick.processing = true;
|
|
|
|
(event.type.includes('Update') ? onUpdate() : onInstall())
|
|
|
|
.then(done, done);
|
|
|
|
function done() {
|
|
|
|
setTimeout(() => {
|
|
|
|
onClick.processing = false;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onInstall() {
|
|
|
|
return getResource(getMeta('stylish-description'))
|
2017-07-12 18:17:04 +00:00
|
|
|
.then(name => saveStyleCode('styleInstall', name))
|
|
|
|
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-08 03:52:51 +00:00
|
|
|
function onUpdate() {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
chrome.runtime.sendMessage({
|
|
|
|
method: 'getStyles',
|
|
|
|
url: getMeta('stylish-id-url') || location.href,
|
|
|
|
}, ([style]) => {
|
|
|
|
saveStyleCode('styleUpdate', style.name, {id: style.id})
|
|
|
|
.then(resolve, reject);
|
|
|
|
});
|
2017-07-12 18:17:04 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function saveStyleCode(message, name, addProps) {
|
2017-11-08 03:52:51 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
2017-11-21 10:33:11 +00:00
|
|
|
const needsConfirmation = message === 'styleInstall' || !saveStyleCode.confirmed;
|
|
|
|
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
2017-11-08 03:52:51 +00:00
|
|
|
reject();
|
2017-07-12 18:17:04 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-11-21 10:33:11 +00:00
|
|
|
saveStyleCode.confirmed = true;
|
2017-07-12 18:17:04 +00:00
|
|
|
enableUpdateButton(false);
|
2017-11-21 10:23:32 +00:00
|
|
|
getStyleJson().then(json => {
|
|
|
|
if (!json) {
|
|
|
|
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
|
|
|
'https://github.com/openstyles/stylus/issues/195');
|
|
|
|
return;
|
|
|
|
}
|
2017-07-12 18:17:04 +00:00
|
|
|
chrome.runtime.sendMessage(
|
2017-11-21 10:23:32 +00:00
|
|
|
Object.assign(json, addProps, {
|
2017-07-12 18:17:04 +00:00
|
|
|
method: 'saveStyle',
|
|
|
|
reason: 'update',
|
|
|
|
}),
|
|
|
|
style => {
|
2017-07-16 18:02:00 +00:00
|
|
|
if (message === 'styleUpdate' && style.updateUrl.includes('?')) {
|
2017-07-12 18:17:04 +00:00
|
|
|
enableUpdateButton(true);
|
|
|
|
} else {
|
|
|
|
sendEvent('styleInstalledChrome');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function enableUpdateButton(state) {
|
|
|
|
const button = document.getElementById('update_style_button');
|
|
|
|
if (button) {
|
|
|
|
button.style.cssText = state ? '' :
|
|
|
|
'pointer-events: none !important; opacity: .25 !important;';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getMeta(name) {
|
|
|
|
const e = document.querySelector(`link[rel="${name}"]`);
|
|
|
|
return e ? e.getAttribute('href') : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getResource(url) {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
if (url.startsWith('#')) {
|
|
|
|
resolve(document.getElementById(url.slice(1)).textContent);
|
|
|
|
} else {
|
|
|
|
chrome.runtime.sendMessage({method: 'download', url}, resolve);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-21 10:23:32 +00:00
|
|
|
function getStyleJson() {
|
|
|
|
const url = getStyleURL();
|
|
|
|
return getResource(url).then(code => {
|
|
|
|
try {
|
|
|
|
return JSON.parse(code);
|
|
|
|
} catch (e) {
|
|
|
|
return fetch(url).then(r => r.json()).catch(() => null);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-07-12 18:17:04 +00:00
|
|
|
function styleSectionsEqual({sections: a}, {sections: b}) {
|
|
|
|
if (!a || !b) {
|
|
|
|
return undefined;
|
|
|
|
}
|
2017-07-16 18:02:00 +00:00
|
|
|
if (a.length !== b.length) {
|
2017-07-12 18:17:04 +00:00
|
|
|
return false;
|
|
|
|
}
|
2017-10-02 14:54:04 +00:00
|
|
|
// order of sections should be identical to account for the case of multiple
|
|
|
|
// sections matching the same URL because the order of rules is part of cascading
|
|
|
|
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
|
2017-07-12 18:17:04 +00:00
|
|
|
|
|
|
|
function propertiesEqual(secA, secB) {
|
|
|
|
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
|
|
|
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2017-07-16 18:02:00 +00:00
|
|
|
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
2017-07-12 18:17:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function equalOrEmpty(a, b, telltale, comparator) {
|
2017-07-16 18:02:00 +00:00
|
|
|
const typeA = a && typeof a[telltale] === 'function';
|
|
|
|
const typeB = b && typeof b[telltale] === 'function';
|
2017-07-12 18:17:04 +00:00
|
|
|
return (
|
|
|
|
(a === null || a === undefined || (typeA && !a.length)) &&
|
|
|
|
(b === null || b === undefined || (typeB && !b.length))
|
2017-07-16 18:02:00 +00:00
|
|
|
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
2017-07-12 18:17:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function arrayMirrors(array1, array2) {
|
2017-10-02 14:54:04 +00:00
|
|
|
return (
|
|
|
|
array1.every(el => array2.includes(el)) &&
|
|
|
|
array2.every(el => array1.includes(el))
|
|
|
|
);
|
2017-07-12 18:17:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onDOMready() {
|
2017-07-16 18:02:00 +00:00
|
|
|
if (document.readyState !== 'loading') {
|
2017-07-12 18:17:04 +00:00
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
|
|
document.addEventListener('DOMContentLoaded', function _() {
|
|
|
|
document.removeEventListener('DOMContentLoaded', _);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function orphanCheck() {
|
|
|
|
const port = chrome.runtime.connect();
|
|
|
|
if (port) {
|
|
|
|
port.disconnect();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// we're orphaned due to an extension update
|
|
|
|
// we can detach event listeners
|
2017-11-08 03:52:51 +00:00
|
|
|
document.removeEventListener('stylishUpdate', onClick);
|
|
|
|
document.removeEventListener('stylishUpdateChrome', onClick);
|
|
|
|
document.removeEventListener('stylishUpdateOpera', onClick);
|
2017-07-12 18:17:04 +00:00
|
|
|
|
2017-11-08 03:52:51 +00:00
|
|
|
document.removeEventListener('stylishInstall', onClick);
|
|
|
|
document.removeEventListener('stylishInstallChrome', onClick);
|
|
|
|
document.removeEventListener('stylishInstallOpera', onClick);
|
2017-07-12 18:17:04 +00:00
|
|
|
|
|
|
|
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
|
|
|
// we can destroy global functions in this context to free up memory
|
|
|
|
[
|
|
|
|
'checkUpdatability',
|
|
|
|
'getMeta',
|
|
|
|
'getResource',
|
|
|
|
'onDOMready',
|
2017-11-08 03:52:51 +00:00
|
|
|
'onClick',
|
|
|
|
'onInstall',
|
|
|
|
'onUpdate',
|
2017-07-12 18:17:04 +00:00
|
|
|
'orphanCheck',
|
|
|
|
'saveStyleCode',
|
|
|
|
'sendEvent',
|
|
|
|
'styleSectionsEqual',
|
|
|
|
].forEach(fn => (window[fn] = null));
|
|
|
|
}
|