-
Notifications
You must be signed in to change notification settings - Fork 314
/
Copy pathusercss-install-helper.js
163 lines (149 loc) · 5.06 KB
/
usercss-install-helper.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import '@/js/browser';
import {kContentType, kMainFrame} from '@/js/consts';
import {DNR_ID_INSTALLER, updateDynamicRules} from '@/js/dnr';
import * as prefs from '@/js/prefs';
import {FIREFOX} from '@/js/ua';
import * as URLS from '@/js/urls';
import {getHost, RX_META} from '@/js/util';
import {bgBusy, onTabUrlChange} from './common';
import download from './download';
import tabCache, * as tabMan from './tab-manager';
import {openURL} from './tab-util';
const installCodeCache = {};
const MIME = 'mime';
export const kUrlInstaller = 'urlInstaller';
bgBusy.then(() => {
prefs.subscribe(kUrlInstaller, toggle, true);
});
export function getInstallCode(url) {
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
const {code, timer} = installCodeCache[url];
clearInstallCode(url);
clearTimeout(timer);
return code;
}
function toggle(key, val, isInit) {
if (val) onTabUrlChange.add(maybeInstall);
else onTabUrlChange.delete(maybeInstall);
if (!__.MV3 || !isInit) toggleUrlInstaller(val);
}
export function toggleUrlInstaller(val) {
const urls = val ? [''] : [
/* Known distribution sites where we ignore urlInstaller option, because
they open .user.css URL only when the "Install" button is clicked.
We can't be sure of it on general-purpose sites like github.com. */
URLS.usw,
...URLS.usoaRaw,
...['greasy', 'sleazy'].map(h => `https://update.${h}fork.org/`),
];
if (__.MV3) {
updateDynamicRules([{
id: DNR_ID_INSTALLER,
condition: {
regexFilter: (val
? /^.*\.user\.(?:css|less|styl)(?:\?.*)?$/
: /^.*\.user\.css$/).source,
requestDomains: val
? undefined
: [...new Set(urls.map(getHost))],
resourceTypes: [kMainFrame],
responseHeaders: [{
header: kContentType,
values: ['text/*'],
excludedValues: ['text/html*'], // * excludes charset and whatnot
}],
},
action: {
type: 'redirect',
redirect: {
regexSubstitution: chrome.runtime.getURL(URLS.installUsercss + '#\\0'),
},
},
}]);
} else {
chrome.webRequest.onHeadersReceived.removeListener(maybeInstallByMime);
chrome.webRequest.onHeadersReceived.addListener(maybeInstallByMime, {
urls: urls.reduce(reduceUsercssGlobs, []),
types: [kMainFrame],
}, ['responseHeaders', 'blocking']);
}
}
function clearInstallCode(url) {
delete installCodeCache[url];
}
/** Ignoring .user.css response that is not a plain text but a web page.
* Not using a whitelist of types as the possibilities are endless e.g. text/x-css-stylus */
function isContentTypeText(type) {
return /^text\/(?!html)/i.test(type);
}
// in Firefox we have to use a content script to read file://
async function loadFromFile(tabId) {
return (await browser.tabs.executeScript(tabId, {
file: `/${__.JS}install-hook-usercss.js`,
}))[0];
}
async function loadFromUrl(tabId, url) {
return (
url.startsWith('file:') ||
tabCache.get(tabId)?.[MIME]
) && download(url);
}
function makeInstallerUrl(url) {
return `${URLS.ownRoot}${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
}
function reduceUsercssGlobs(res, host) {
res.push(...'%css,%less,%styl'
.replace(/%\w+/g, host ? '$&*' : '$&,$&?*')
.replace(/%/g, `${host || '*://*/'}*.user.`)
.split(','));
return res;
}
async function maybeInstall(tabId, url, oldUrl = '') {
if (url.includes('.user.') &&
tabCache.get(tabId)?.[MIME] !== false &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|less|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
!oldUrl.startsWith(makeInstallerUrl(url))) {
const inTab = FIREFOX && url.startsWith('file:');
const code = await (inTab ? loadFromFile : loadFromUrl)(tabId, url);
if (!/^\s*</.test(code) && RX_META.test(code)) {
await openInstallerPage(tabId, url, {code, inTab});
}
}
}
function maybeInstallByMime({tabId, url, responseHeaders}) {
const h = responseHeaders.find(_ => _.name.toLowerCase() === kContentType);
const isText = h && isContentTypeText(h.value);
tabMan.set(tabId, MIME, isText);
if (isText) {
openInstallerPage(tabId, url, {});
// Silently suppress navigation.
// Don't redirect to the install URL as it'll flash the text!
return {cancel: true};
}
}
async function openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = makeInstallerUrl(url);
if (inTab) {
const tab = await browser.tabs.get(tabId);
return openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
});
}
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache[url] = {code, timer};
try {
await browser.tabs.update(tabId, {url: newUrl});
} catch (err) {
// FIXME: remove this when kiwi supports tabs.update
// https://github.com/openstyles/stylus/issues/1367
if (/Tabs cannot be edited right now/i.test(err.message)) {
return browser.tabs.create({url: newUrl});
}
throw err;
}
}