Skip to content

Commit

Permalink
Completely rewrite user script download/install.
Browse files Browse the repository at this point in the history
One Downloader class knows how to download whatever things necessary, for new install or edit/update case.  Knows how to send it to the registry to be stored for install (including update) as well.
Script install detection method shows only the install dialog, aborting navigation and launching download instead.

Fixes greasemonkey#2817
  • Loading branch information
arantius committed Feb 22, 2018
1 parent 2757a3a commit 65f2912
Show file tree
Hide file tree
Showing 18 changed files with 425 additions and 491 deletions.
26 changes: 2 additions & 24 deletions doc/Messages.md
Expand Up @@ -67,17 +67,6 @@ Data:
* `uuid` The UUID of an installed script which is storing this value.
* `value` The new value to store.

# EditorSaved
Sent by: `content/edit-user-script.js`.
Received by: `bg/user-script-registry.js`.

Sent whenever the user triggers the save action in the user script editor.
Data:

* `uuid` String UUID of the script being edited.
* `content` String text content of main script.
* `requires` Object mapping require URL to text content.

# EnabledChanged
Sent by: `bg/is-enabled.js`.

Expand Down Expand Up @@ -114,17 +103,6 @@ Response data:

* An array of `.details` objects from installed `RunnableUserScript`s.

# InstallProgress
Sent by: `downloader.js`
Received by: `content/install-dialog.js`

While downloading a user script (and all dependencies), reports the current
progress as a percentage. Sent specifically back to the content process
(tab / frame) which started the install. Data:

* `errors` A (possibly empty) list of string error messages.
* `progress` A number, 0.0 to 1.0, representing the completion so far.

# UserScriptChanged
Sent by: `bg/user-script-registry.js`

Expand All @@ -145,8 +123,8 @@ Response:
* `details` The details object from an `EditableUserScript`.

# UserScriptInstall
Sent by: `content/install-dialog.js`
Received by: `downloader.js`
Sent by: `downloader.js`
Received by: `bg/user-script-registry.js`

Triggered when the install button of the install dialog is clicked by the
user. Data:
Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
Expand Up @@ -8,6 +8,7 @@ module.exports = function(config) {
'./test/**/*.test.js',
],
exclude: [
'./src/i18n.js', // Use test set up version instead.
'./src/**/*.run.js',
'./src/content/edit-user-script.js', // CodeMirror dependency.
'./src/content/install-dialog.js', // Not ready for testing yet. TODO!
Expand Down
3 changes: 2 additions & 1 deletion manifest.json
Expand Up @@ -42,10 +42,11 @@
"src/downloader.js",
"src/parse-meta-line.js",
"src/parse-user-script.js",
"src/supported-apis.js",
"src/user-script-obj.js",
"src/util/check-api-call-allowed.js",
"third-party/MatchPattern.js",
"third-party/convert2RegExp.js",
"third-party/MatchPattern.js",

"src/bg/execute.run.js",
"src/bg/user-script-detect.run.js",
Expand Down
10 changes: 0 additions & 10 deletions src/bg/api-provider-source.js
Expand Up @@ -6,16 +6,6 @@ This will be an anonymous and immediately called function which exports objects
to the global scope (the `this` object). It ...
*/

const SUPPORTED_APIS = new Set([
'GM.deleteValue', 'GM.getValue', 'GM.listValues', 'GM.setValue',
'GM.getResourceUrl',
'GM.notification',
'GM.openInTab',
'GM.setClipboard',
'GM.xmlHttpRequest',
]);


(function() {

function apiProviderSource(userScript) {
Expand Down
138 changes: 45 additions & 93 deletions src/bg/user-script-detect.js
@@ -1,113 +1,65 @@
/* Add listeners to detect user scripts and open the installation dialog. */
/* Detect user scripts, possibly open the installation dialog. */

(function() {

const userScriptTypes = [
'text/plain',
'application/ecmascript',
'application/javascript',
'application/x-javascript',
'text/ecmascript',
'text/javascript',
];
const contentTypeRe = new RegExp(`(${userScriptTypes.join('|')})(;.*)?`);
const gContentTypeRe = (() => {
const userScriptTypes = [
'text/plain',
'application/ecmascript',
'application/javascript',
'application/x-javascript',
'text/ecmascript',
'text/javascript',
];
return new RegExp(`^(${userScriptTypes.join('|')})\\b`);
})();

function catchParseUserScript(userScriptContent, url) {
try {
return parseUserScript(userScriptContent, url, true);
} catch (err) {
// It's not important why the parse failed or threw. Just treat it as the
// parsing was unsuccessful and fetch more data.
// Log the error so it isn't silently dismissed.
// TODO: This may flood the console
console.info('Detect script parse error', err);
return false;
}

function onHeadersReceivedDetectUserScript(requestDetails) {
if (!getGlobalEnabled()) return {};
if (requestDetails.method != 'GET') return {};
if (!responseHasUserScriptType(requestDetails.responseHeaders)) return {};

openInstallDialog(requestDetails.url);

// https://stackoverflow.com/a/18684302
return {'redirectUrl': 'javascript:'};
}
window.onHeadersReceivedDetectUserScript = onHeadersReceivedDetectUserScript;


// Examine headers before determining if script checking is needed
function checkHeaders(responseHeaders) {
function responseHasUserScriptType(responseHeaders) {
for (header of responseHeaders) {
let headerName = header.name.toLowerCase();
if ('content-type' === headerName && contentTypeRe.test(header.value)) {
if ('content-type' === headerName && gContentTypeRe.test(header.value)) {
return true;
}
}
return false;
}


// Check if enough content is available to open an install message
function checkScript(userScriptContent, url) {
let scriptDetails = catchParseUserScript(userScriptContent, url);
if (scriptDetails) {
openInstallDialog(scriptDetails, url);
return true;
} else {
return false;
}
}


function detectUserScriptOnHeadersReceived(details) {
if (!getGlobalEnabled() || !checkHeaders(details.responseHeaders)) {
return {};
}

let decoder = new TextDecoder("utf-8");
let encoder = new TextEncoder();
let filter = chrome.webRequest.filterResponseData(details.requestId);

let userScriptContent = '';

filter.ondata = event => {
userScriptContent = userScriptContent
+ decoder.decode(event.data, {'stream': true});
if (checkScript(userScriptContent, details.url)) {
// We have enough for the details. Since we use a new window for install
// the filter can be flushed and disconnected so that Firefox handles
// the rest of the data normally.
filter.write(encoder.encode(userScriptContent));
filter.disconnect();
}
};
filter.onstop = event => {
// One last check to see if we have a valid script.
checkScript(userScriptContent, details.url);
// Regardless, since we use a new window just flush the filter and close.
filter.write(encoder.encode(userScriptContent));
filter.close();
};

return {};
}
window.detectUserScriptOnHeadersReceived = detectUserScriptOnHeadersReceived;


// Open platform specific installation dialog
function openInstallDialog(scriptDetails, url) {
function openInstallDialog(url) {
chrome.runtime.getPlatformInfo(platform => {
let installUrl = chrome.runtime.getURL('src/content/install-dialog.html')
+ '?' + escape(JSON.stringify(scriptDetails));

if ('android' === platform.os) {
chrome.tabs.create({'active': true, 'url': installUrl});
} else {
let options = {
'height': 640,
'titlePreface': _('$1 - Greasemonkey User Script', scriptDetails.name),
'type': 'popup',
'url': installUrl,
'width': 480,
};
chrome.windows.create(options, newWindow => {
// Fix for Fx57 bug where bundled page loaded using
// browser.windows.create won't show contents unless resized.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1402110
chrome.windows.update(newWindow.id, {width: newWindow.width + 1});
});
}
let installUrl = chrome.runtime.getURL('src/content/install-dialog.html')
+ '?' + escape(url);

if ('android' === platform.os) {
chrome.tabs.create({'active': true, 'url': installUrl});
} else {
let options = {
'height': 640,
'type': 'popup',
'url': installUrl,
'width': 480,
};
chrome.windows.create(options, newWindow => {
// Fix for Fx57 bug where bundled page loaded using
// browser.windows.create won't show contents unless resized.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1402110
chrome.windows.update(newWindow.id, {width: newWindow.width + 1});
});
}
});
}

Expand Down
9 changes: 4 additions & 5 deletions src/bg/user-script-detect.run.js
@@ -1,5 +1,4 @@
browser.webRequest.onHeadersReceived.addListener(
detectUserScriptOnHeadersReceived,
{'urls': ['*://*/*.user.js'], 'types': ['main_frame']},
['blocking', 'responseHeaders']
);
chrome.webRequest.onHeadersReceived.addListener(
onHeadersReceivedDetectUserScript,
{'urls': ['*://*/*.user.js'], 'types': ['main_frame']},
['blocking', 'responseHeaders']);
76 changes: 19 additions & 57 deletions src/bg/user-script-registry.js
Expand Up @@ -44,60 +44,33 @@ async function openDb() {
});
}


///////////////////////////////////////////////////////////////////////////////

async function installFromDownloader(downloader) {
async function installFromDownloader(userScriptDetails, downloaderDetails) {
let db = await openDb();
try {
let remoteScript = new RemoteUserScript(downloader.scriptDetails);
let txn = db.transaction([scriptStoreName], "readonly");
let store = txn.objectStore(scriptStoreName);
let index = store.index('id');
let req = index.get(remoteScript.id);
txn.oncomplete = event => {
let userScript = new EditableUserScript(req.result || {});
userScript.updateFromDownloader(downloader);
saveUserScript(userScript);
db.close();
// TODO: Notification?
};
txn.onerror = event => {
console.error('Error looking up script!', event);
db.close();
};
} catch (e) {
console.error('at installFromDownloader(), db fail:', e);
db.close();
}
}


async function installFromSource(source) {
let db = await openDb();
return new Promise((resolve, reject) => {
return new Promise(async (resolve, reject) => {
try {
let details = parseUserScript(source, null);
let remoteScript = new RemoteUserScript(details);
let remoteScript = new RemoteUserScript(userScriptDetails);
let txn = db.transaction([scriptStoreName], "readonly");
let store = txn.objectStore(scriptStoreName);
let index = store.index('id');
let req = index.get(remoteScript.id);
txn.oncomplete = event => {
details = req.result || details;
details.content = source;
details.parsedDetails = details;
let userScript = new EditableUserScript(details);
saveUserScript(userScript);
txn.oncomplete = async event => {
let userScript = new EditableUserScript(req.result || {});
userScript.updateFromDownloaderDetails(
userScriptDetails, downloaderDetails);
await saveUserScript(userScript);
resolve(userScript.uuid);
db.close();
};
txn.onerror = event => {
console.error('Error looking up script!', event);
reject();
db.close();
};
} catch (e) {
console.error('at installFromSource(), db fail:', e);
console.error('at installFromDownloader(), db fail:', e);
reject();
db.close();
}
});
Expand Down Expand Up @@ -131,23 +104,6 @@ async function loadUserScripts() {
}


function onEditorSaved(message, sender, sendResponse) {
let userScript = userScripts[message.uuid];
if (!userScript) {
console.error('Got save for UUID', message.uuid, 'but it does not exist.');
return;
}

// Use a clone of the current user script. This is so that any changes are
// not propagated to the actual UserScript unless the transaction is
// successful.
let cloneScript = new EditableUserScript(userScript.details);
cloneScript.updateFromEditorSaved(message)
.then(value => saveUserScript(cloneScript));
};
window.onEditorSaved = onEditorSaved;


function onListUserScripts(message, sender, sendResponse) {
let result = [];
var userScriptIterator = UserScriptRegistry.scriptsToRunAt(
Expand All @@ -173,6 +129,14 @@ function onUserScriptGet(message, sender, sendResponse) {
window.onUserScriptGet = onUserScriptGet;


window.onUserScriptInstall = async function(message, sender, sendResponse) {
let uuid
= await installFromDownloader(message.userScript, message.downloader);
sendResponse(uuid);
return uuid;
}


function onApiGetResourceBlob(message, sender, sendResponse) {
if (!message.uuid) {
console.error('onApiGetResourceBlob handler got no UUID.');
Expand Down Expand Up @@ -326,8 +290,6 @@ function* scriptsToRunAt(urlStr=null, includeDisabled=false) {
window.UserScriptRegistry = {
'_loadUserScripts': loadUserScripts,
'_saveUserScript': saveUserScript,
'installFromDownloader': installFromDownloader,
'installFromSource': installFromSource,
'scriptByUuid': scriptByUuid,
'scriptsToRunAt': scriptsToRunAt,
};
Expand Down
1 change: 1 addition & 0 deletions src/browser/monkey-menu.html
Expand Up @@ -127,6 +127,7 @@
<script src="/third-party/MatchPattern.js"></script>
<script src="/src/util/iconUrl.js"></script>
<script src="/src/util/rivets-formatters.js"></script>
<script src="/src/downloader.js"></script>
<script src="/src/parse-meta-line.js"></script>
<script src="/src/parse-user-script.js"></script>
<script src="/src/user-script-obj.js"></script>
Expand Down

0 comments on commit 65f2912

Please sign in to comment.