Skip to content
Browse files

RemoteScript module; replaces GM_scriptDownloader.

Refs #1458
  • Loading branch information...
1 parent ea0568e commit 268f716d06117ee82e62b93bb1d6dfb283c2e6ec @arantius arantius committed Nov 11, 2011
View
46 components/greasemonkey.js
@@ -17,7 +17,7 @@ var gExtensionPath = (function() {
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var uri = ioService.newURI(Components.stack.filename, null, null);
- var file = uri.QueryInterface(Components.interfaces.nsIFileURL).file
+ var file = uri.QueryInterface(Components.interfaces.nsIFileURL).file;
// ... to find the containing directory.
var dir = file.parent.parent;
// Then get the URL back for that path.
@@ -31,6 +31,9 @@ var gMaxJSVersion = "1.8";
var gMenuCommands = [];
var gStartupHasRun = false;
+var gWindowWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(Ci.nsIWindowWatcher);
+
/////////////////////// Component-global Helper Functions //////////////////////
// TODO: Remove this, see #1318.
@@ -146,6 +149,41 @@ function getFirebugConsole(wrappedContentWin, chromeWin) {
}
}
+function installDialog(aUri, aBrowser, aService) {
+ var scope = {};
+ Cu.import('resource://greasemonkey/remoteScript.js', scope);
+ var rs = new scope.RemoteScript(aUri.spec);
+
+ rs.onScriptMeta(function(aRemoteScript, aType, aScript) {
+ var params = [rs, aScript];
+ params.wrappedJSObject = params;
+ // TODO: Find a better fix than this sloppy workaround.
+ // Apparently this version of .openWindow() blocks; and as called by the
+ // "script meta data available" callback as this is, blocks the further
+ // download of the script!
+ var curriedOpenWindow = GM_util.hitch(
+ null, gWindowWatcher.openWindow,
+ /* aParent */ null,
+ 'chrome://greasemonkey/content/install.xul',
+ /* aName */ null,
+ 'chrome,centerscreen,modal,dialog,titlebar,resizable',
+ params);
+ GM_util.timeout(0, curriedOpenWindow);
+ });
+
+ rs.download(function(aSuccess, aType) {
+ if (!aSuccess) {
+ // Failure downloading script; browse to it.
+ aService.ignoreNextScript();
+ aBrowser.loadURI(aUri.spec, /* aReferrer */ null, /* aCharset */ null);
+// } else if (aSuccess && 'script' == aType) {
+// dump('script is downloaded.\n');
+// } else if (aSuccess && 'dependencies' == aType) {
+// dump('script and all dependencies downloaded.\n');
+ }
+ });
+}
+
function isTempScript(uri) {
if (uri.scheme != "file") return false;
@@ -337,10 +375,8 @@ service.prototype.shouldLoad = function(ct, cl, org, ctx, mt, ext) {
|| ct == Ci.nsIContentPolicy.TYPE_SUBDOCUMENT)
&& cl.spec.match(/\.user\.js$/)
) {
- if (!this._ignoreNextScript
- && !isTempScript(cl)
- && GM_util.installUri(cl, ctx.contentWindow)
- ) {
+ if (!this._ignoreNextScript && !isTempScript(cl)) {
+ installDialog(cl, ctx, this);
ret = Ci.nsIContentPolicy.REJECT_REQUEST;
}
}
View
17 content/browser.js
@@ -165,23 +165,6 @@ GM_BrowserUI.showInstallBanner = function(browser) {
);
};
-/**
- * Called from greasemonkey service when we should load a user script.
- */
-GM_BrowserUI.startInstallScript = function(uri, contentWin, timer) {
- if (!timer) {
- // docs for nsicontentpolicy say we're not supposed to block, so short
- // timer.
- window.setTimeout(
- GM_BrowserUI.startInstallScript, 0, uri, contentWin, true);
- return;
- }
-
- GM_BrowserUI._scriptDownloader =
- new GM_ScriptDownloader(window, uri, GM_BrowserUI.bundle, contentWin);
- GM_BrowserUI._scriptDownloader.startInstall();
-};
-
/**
* Open the tab to show the contents of a script and display the banner to let
View
16 content/config.js
@@ -311,22 +311,6 @@ Config.prototype.install = function(script, oldScript) {
this.uninstall(oldScript, true);
}
- script._initFile(script._tempFile);
- script._tempFile = null;
-
- // if icon had to be downloaded, then move the file
- if (script.icon.hasDownloadURL()) {
- script.icon._initFile();
- }
-
- for (var i = 0; i < script._requires.length; i++) {
- script._requires[i]._initFile();
- }
-
- for (var i = 0; i < script._resources.length; i++) {
- script._resources[i]._initFile();
- }
-
script._modified = script.file.lastModifiedTime;
script._dependhash = GM_util.sha1(script._rawMeta);
View
207 content/install.js
@@ -1,121 +1,136 @@
-var GMInstall = {
- init: function() {
- this._htmlNs = "http://www.w3.org/1999/xhtml";
-
- this._scriptDownloader = window.arguments[0];
- this._script = this._scriptDownloader.script;
-
- this.setupIncludes("includes", "includes-desc", this._script.includes);
- this.setupIncludes("excludes", "excludes-desc", this._script.excludes);
- var matches = [];
- for (var i = 0, match = null; match = this._script.matches[i]; i++) {
- matches.push(match.pattern);
- }
- this.setupIncludes("matches", "matches-desc", matches);
+var gRemoteScript = window.arguments[0].wrappedJSObject[0];
+var gScript = window.arguments[0].wrappedJSObject[1];
+var gHtmlNs = 'http://www.w3.org/1999/xhtml';
+
+var gAcceptButton = null;
+var gCurrentDelay = null;
+var gProgress = 0;
+var gTimer = null;
+var gTotalDelay = 5;
+
+function init() {
+ setUpIncludes('includes', 'includes-desc', gScript.includes);
+ setUpIncludes('excludes', 'excludes-desc', gScript.excludes);
+
+ var matches = [];
+ for (var i = 0, match = null; match = gScript.matches[i]; i++) {
+ matches.push(match.pattern);
+ }
+ setUpIncludes('matches', 'matches-desc', matches);
- this._dialog = document.documentElement;
- this._extraButton = this._dialog.getButton("extra1");
- this._extraButton.setAttribute("type", "checkbox");
+ document.documentElement.getButton('extra1').setAttribute('type', 'checkbox');
- this._acceptButton = this._dialog.getButton("accept");
- this._acceptButton.baseLabel = this._acceptButton.label;
+ gAcceptButton = document.documentElement.getButton('accept');
+ gAcceptButton.baseLabel = gAcceptButton.label;
- this._timer = null;
- this._seconds = 0;
- this.startTimer();
+ startTimer();
- this.bundle = document.getElementById("gm-browser-bundle");
+ var bundle = document.getElementById('gm-browser-bundle');
- var heading = document.getElementById("heading");
- heading.appendChild(document.createTextNode(
- this.bundle.getString("greeting.msg")));
+ document.getElementById('heading').appendChild(
+ document.createTextNode(bundle.getString('greeting.msg')));
- var desc = document.getElementById("scriptDescription");
- desc.appendChild(document.createElementNS(this._htmlNs, "strong"));
- desc.firstChild.appendChild(document.createTextNode(this._script.name));
- if (this._script.version) {
- desc.appendChild(document.createTextNode(' ' + this._script.version));
- }
- desc.appendChild(document.createElementNS(this._htmlNs, "br"));
- desc.appendChild(document.createTextNode(this._script.description));
- },
+ var desc = document.getElementById('scriptDescription');
+ desc.appendChild(document.createElementNS(gHtmlNs, 'strong'));
+ desc.firstChild.appendChild(document.createTextNode(gScript.name));
+ if (gScript.version) {
+ desc.appendChild(document.createTextNode(' ' + gScript.version));
+ }
+ desc.appendChild(document.createElementNS(gHtmlNs, 'br'));
+ desc.appendChild(document.createTextNode(gScript.description));
+
+ if (gRemoteScript.done) {
+ // Download finished before we could open, fake a progress event.
+ onProgress(null, null, 1);
+ } else {
+ // Otherwise, listen for future progress events.
+ gRemoteScript.onProgress(onProgress);
+ }
+}
- onFocus: function(e) {
- this.startTimer();
- },
+function onBlur(e) {
+ stopTimer();
+}
- onBlur: function(e) {
- this.stopTimer();
- },
+function onCancel() {
+ gRemoteScript.cleanup();
+ window.close();
+}
- startTimer: function() {
- this._seconds = 4;
- this.updateLabel();
+function onFocus(e) {
+ startTimer();
+}
- if (this._timer) {
- window.clearInterval(this._timer);
- }
+function onInterval() {
+ gCurrentDelay--;
+ updateLabel();
- this._timer = window.setInterval(function() { GMInstall.onInterval(); }, 500);
- },
+ if (gCurrentDelay == 0) stopTimer();
+}
- onInterval: function() {
- this._seconds--;
- this.updateLabel();
+function onOk() {
+ gRemoteScript.install();
+ window.setTimeout(window.close, 0);
+}
- if (this._seconds == 0) {
- this._timer = window.clearInterval(this._timer);
- }
- },
-
- stopTimer: function() {
- this._seconds = 5;
- this._timer = window.clearInterval(this._timer);
- this.updateLabel();
- },
-
- updateLabel: function() {
- if (this._seconds > 0) {
- this._acceptButton.focus();
- this._acceptButton.disabled = true;
- this._acceptButton.label = this._acceptButton.baseLabel + " (" + this._seconds + ")";
- } else {
- this._acceptButton.disabled = false;
- this._acceptButton.label = this._acceptButton.baseLabel;
- }
- },
+function onProgress(aRemoteScript, aEventType, aData) {
+ if (!document) return; // lingering download after window cancel
+ gProgress = Math.floor(100 * aData);
+ if (1 == aData) {
+ document.getElementById('loading').style.display = 'none';
+ } else {
+ document.getElementById('progressmeter').setAttribute('value', gProgress);
+ }
+ updateLabel();
+}
- setupIncludes: function(box, desc, includes) {
- if (includes.length > 0) {
- desc = document.getElementById(desc);
- document.getElementById(box).style.display = "";
+function onShowSource() {
+ // _scriptDownloader.showScriptView();
+ window.setTimeout(window.close, 0);
+}
- for (var i = 0; i < includes.length; i++) {
- desc.appendChild(document.createTextNode(includes[i]));
- desc.appendChild(document.createElementNS(this._htmlNs, "br"));
- }
+function pauseTimer() {
+ stopTimer();
+ gCurrentDelay = gTotalDelay;
+ updateLabel();
+}
+
+function setUpIncludes(box, desc, includes) {
+ if (includes.length > 0) {
+ desc = document.getElementById(desc);
+ document.getElementById(box).style.display = '';
- desc.removeChild(desc.lastChild);
+ for (var i = 0; i < includes.length; i++) {
+ desc.appendChild(document.createTextNode(includes[i]));
+ desc.appendChild(document.createElementNS(gHtmlNs, 'br'));
}
- },
- onOK: function() {
- this._scriptDownloader.installScript();
- window.setTimeout(window.close, 0);
- },
+ desc.removeChild(desc.lastChild);
+ }
+}
+
+function startTimer() {
+ gCurrentDelay = gTotalDelay;
+ updateLabel();
+
+ gTimer = window.setInterval(onInterval, 500);
+}
- onCancel: function(){
- this._scriptDownloader.cleanupTempFiles();
- window.close();
- },
+function stopTimer() {
+ if (gTimer) window.clearInterval(gTimer);
+}
- onShowSource: function() {
- this._scriptDownloader.showScriptView();
- window.setTimeout(window.close, 0);
+function updateLabel() {
+ if (gCurrentDelay > 0) {
+ gAcceptButton.focus();
+ gAcceptButton.label = gAcceptButton.baseLabel + ' (' + gCurrentDelay + ')';
+ } else {
+ gAcceptButton.label = gAcceptButton.baseLabel;
}
-};
+ gAcceptButton.disabled = (gCurrentDelay > 0) || (gProgress < 100);
+}
// See: closewindow.xul .
function GM_onClose() {
- GMInstall.onCancel();
+ gRemoteScript.cleanup();
}
View
14 content/install.xul
@@ -13,18 +13,17 @@
id="greasemonkey"
title="&install.title;"
style="width: 32em; height:32em;"
- onload="GMInstall.init();"
- ondialogaccept="return GMInstall.onOK();"
- ondialogcancel="return GMInstall.onCancel();"
- ondialogextra1="return GMInstall.onShowSource();"
+ onload="init();"
+ ondialogaccept="return onOk();"
+ ondialogcancel="return onCancel();"
+ ondialogextra1="return onShowSource();"
buttons="accept,cancel,extra1"
buttonlabelaccept="&install.installbutton;"
buttonlabelextra1="&install.showscriptsource;"
defaultButton="accept">
<stringbundle id="gm-browser-bundle" src="chrome://greasemonkey/locale/gm-browser.properties" />
- <script type="application/x-javascript" src="chrome://greasemonkey/content/scriptdownloader.js" />
<script type="application/x-javascript" src="chrome://greasemonkey/content/install.js" />
<vbox id="dialogContentBox"
@@ -76,6 +75,11 @@
style="margin-bottom:1em"
class="warning"
>&install.warning2;</description>
+
+ <vbox id="loading">
+ <label value="&loading;" />
+ <progressmeter id="progressmeter"/>
+ </vbox>
</vbox>
</dialog>
View
44 content/script.js
@@ -203,28 +203,9 @@ function Script_getFileURL() { return GM_util.getUriFromFile(this.file).spec; })
Script.prototype.__defineGetter__('textContent',
function Script_getTextContent() { return GM_util.getContents(this.file); });
-Script.prototype._initFileName = function(name, useExt) {
- var ext = "";
- name = name.toLowerCase();
-
- var dotIndex = name.lastIndexOf(".");
- if (dotIndex > 0 && useExt) {
- ext = name.substring(dotIndex + 1);
- name = name.substring(0, dotIndex);
- }
-
- name = name.replace(/\s+/g, "_").replace(/[^-_A-Z0-9]+/gi, "");
- ext = ext.replace(/\s+/g, "_").replace(/[^-_A-Z0-9]+/gi, "");
-
- // If no Latin characters found - use default
- if (!name) name = "gm_script";
-
- // 24 is a totally arbitrary max length
- if (name.length > 24) name = name.substring(0, 24);
-
- if (ext) name += "." + ext;
-
- return name;
+Script.prototype.setFilename = function(aBaseName, aFileName) {
+ this._basedir = aBaseName;
+ this._filename = aFileName;
};
Script.prototype._loadFromConfigNode = function(node) {
@@ -408,25 +389,6 @@ Script.prototype.toString = function() {
return '[Greasemonkey Script ' + this.id + ']';
};
-Script.prototype._initFile = function(tempFile) {
- var name = this._initFileName(this._name, false);
- this._basedir = name;
-
- var nsIFile = Components.interfaces.nsIFile;
- var file = GM_util.scriptDir();
- file.append(name);
- file.createUnique(nsIFile.DIRECTORY_TYPE, GM_constants.directoryMask);
- this._basedir = file.leafName;
-
- file.append(name + ".user.js");
- file.createUnique(nsIFile.NORMAL_FILE_TYPE, GM_constants.fileMask);
- this._filename = file.leafName;
-
- file.remove(true);
- tempFile.moveTo(file.parent, file.leafName);
-};
-
-
Script.prototype.__defineGetter__('urlToDownload',
function Script_getUrlToDownload() { return this._downloadURL; });
View
1 content/scripticon.js
@@ -4,6 +4,7 @@ function ScriptIcon(script) {
ScriptResource.call(this, script);
this.type = "icon";
this._dataURI = null;
+ this._downloadURL = null;
this.dataUriError = false;
}
View
29 content/scriptrequire.js
@@ -5,7 +5,6 @@ function ScriptRequire(script) {
this._script = script;
this._downloadURL = null;
- this._tempFile = null;
this._filename = null;
this.type = "require";
this.updateScript = false;
@@ -18,6 +17,9 @@ function ScriptRequire_getFile() {
return file;
});
+ScriptRequire.prototype.__defineGetter__("filename",
+function ScriptRequire_getFilename() { return new String(this._filename); });
+
ScriptRequire.prototype.__defineGetter__('fileURL',
function ScriptRequire_getFileURL() {
return GM_util.getUriFromFile(this.file).spec;
@@ -29,26 +31,7 @@ function ScriptRequire_getTextContent() { return GM_util.getContents(this.file);
ScriptRequire.prototype.__defineGetter__('urlToDownload',
function ScriptRequire_getUrlToDownload() { return this._downloadURL; });
-ScriptRequire.prototype._initFile = function() {
- var name = this._downloadURL.substr(this._downloadURL.lastIndexOf("/") + 1);
- if(name.indexOf("?") > 0) {
- name = name.substr(0, name.indexOf("?"));
- }
- name = this._script._initFileName(name, true);
-
- var file = this._script._basedirFile;
- file.append(name);
- file.createUnique(
- Components.interfaces.nsIFile.NORMAL_FILE_TYPE, GM_constants.fileMask);
- this._filename = file.leafName;
-
- file.remove(true);
- this._tempFile.moveTo(file.parent, file.leafName);
- this._tempFile = null;
-};
-
-ScriptRequire.prototype.setDownloadedFile = function(file) {
- this._tempFile = file;
- if (this.updateScript)
- this._initFile();
+ScriptRequire.prototype.setFilename = function(aFile) {
+ aFile.QueryInterface(Components.interfaces.nsILocalFile);
+ this._filename = aFile.leafName;
};
View
27 content/scriptresource.js
@@ -14,6 +14,10 @@ function ScriptResource(script) {
this._name = null;
}
+ScriptResource.prototype.toString = function() {
+ return '[ScriptResource; ' + this._downloadURL + ']';
+}
+
ScriptResource.prototype.__defineGetter__('name',
function ScriptResource_getName() { return this._name; });
@@ -24,6 +28,9 @@ function ScriptResource_getFile() {
return file;
});
+ScriptResource.prototype.__defineGetter__("filename",
+function ScriptResource_getFilename() { return new String(this._filename); });
+
ScriptResource.prototype.__defineGetter__('textContent',
function ScriptResource_getTextContent() { return GM_util.getContents(this.file); });
@@ -44,15 +51,17 @@ function ScriptResource_getDataContent() {
+ ";base64," + window.encodeURIComponent(window.btoa(binaryContents));
});
-ScriptResource.prototype._initFile = ScriptRequire.prototype._initFile;
-
ScriptResource.prototype.__defineGetter__('urlToDownload',
function ScriptResource_getUrlToDownload() { return this._downloadURL; });
-ScriptResource.prototype.setDownloadedFile =
-function(tempFile, mimetype, charset) {
- this._tempFile = tempFile;
- this._mimetype = mimetype;
- this._charset = charset;
- if (this.updateScript) this._initFile();
-};
+ScriptResource.prototype.setFilename = ScriptRequire.prototype.setFilename;
+
+/** This should only be called as part of the download process. */
+ScriptResource.prototype.setMimetype = function(aMimetype) {
+ this._mimetype = aMimetype;
+}
+
+/** This should only be called as part of the download process. */
+ScriptResource.prototype.setCharset = function(aCharset) {
+ this._charset = aCharset;
+}
View
1 locale/en-US/greasemonkey.dtd
@@ -20,6 +20,7 @@
<!ENTITY install.warning2 "You should only install scripts from sources that you trust.">
<!ENTITY install.showscriptsource "Show Script Source">
<!ENTITY install.installbutton "Install">
+<!ENTITY loading "Downloading ...">
<!ENTITY newscript.name "Name">
<!ENTITY newscript.namespace "Namespace">
<!ENTITY newscript.description "Description">
View
387 modules/remoteScript.js
@@ -0,0 +1,387 @@
+var EXPORTED_SYMBOLS = ['RemoteScript'];
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import('resource://greasemonkey/util.js');
+
+// Load in the Script objects, not yet module-ized.
+var loader = Cc['@mozilla.org/moz/jssubscript-loader;1']
+ .getService(Ci.mozIJSSubScriptLoader);
+loader.loadSubScript('chrome://greasemonkey/content/script.js');
+loader.loadSubScript('chrome://greasemonkey/content/scriptrequire.js');
+loader.loadSubScript('chrome://greasemonkey/content/scriptresource.js');
+loader.loadSubScript('chrome://greasemonkey/content/scripticon.js');
+
+var GM_config = GM_util.getService().config;
+var ioService = Cc['@mozilla.org/network/io-service;1']
+ .getService(Ci.nsIIOService);
+
+/////////////////////////////// Private Helpers ////////////////////////////////
+
+function assertIsFunction(aFunc, aMessage) {
+ if (typeof aFunc !== typeof function() {}) throw Error(aMessage);
+}
+
+var disallowedFilenameCharacters = new RegExp('[\\\\/:*?\'"<>|]', 'g');
+function cleanFilename(aFilename, aDefault) {
+ // Blacklist problem characters (slashes, colons, etc.).
+ var filename = (aFilename || aDefault).replace(
+ disallowedFilenameCharacters, '');
+ // Ensure that it's something.
+ if (!filename) filename = aDefault || 'unknown';
+ return filename;
+}
+
+function filenameFromUri(aUri, aDefault) {
+ var filename = '';
+ try {
+ var url = aUri.QueryInterface(Ci.nsIURL);
+ filename = url.fileName;
+ } catch (e) {
+ dump('filenameFromUri error: ' + e + '\n');
+ }
+
+ return cleanFilename(filename, aDefault);
+}
+
+////////////////////////// Private Progress Listener ///////////////////////////
+
+function ProgressListener(
+ aRemoteScript, aCompletionCallback, aProgressCallback) {
+ this._remoteScript = aRemoteScript;
+ this._completionCallback = aCompletionCallback || function() {};
+ this._progressCallback = aProgressCallback || function() {};
+}
+
+ProgressListener.prototype.onLocationChange = function(
+ aWebProgress, aRequest, aLocation) {
+};
+
+ProgressListener.prototype.onProgressChange = function(
+ aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ var progress = aCurTotalProgress / aMaxTotalProgress;
+ if (-1 == aMaxTotalProgress) progress = 0;
+
+ if (!this._progressCallback(progress)) {
+ // The progress callback is where we check for HTML type, and return false
+ // if so. In such a case, immediately complete as a failure.
+ this._completionCallback(false, 'any');
+ }
+};
+
+ProgressListener.prototype.onSecurityChange = function(
+ aWebProgress, aRequest, aState) {
+};
+
+/** Called at least at the start and stop of the request. */
+ProgressListener.prototype.onStateChange = function(
+ aWebProgress, aRequest, aStateFlags, aStatus) {
+ // Find if there has been any error.
+ if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)) {
+ return;
+ }
+ var error = aStatus !== 0;
+ try {
+ var httpChannel = aRequest.QueryInterface(Ci.nsIHttpChannel);
+ error |= !httpChannel.requestSucceeded;
+ error |= httpChannel.responseStatus >= 400;
+ } catch (e) {
+ try {
+ aRequest.QueryInterface(Ci.nsIFileChannel);
+ // no-op; if it got this far, aStatus is accurate.
+ } catch (e) {
+ dump('aRequest is neither http nor file channel: ' + aRequest + '\n');
+ for (i in Ci) {
+ try {
+ aRequest.QueryInterface(Ci[i]);
+ dump('it is a: ' + i + '\n');
+ } catch (e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ // Indicate final progress (complete).
+ this._progressCallback(1);
+ // Call back with that found error state.
+ this._completionCallback(!error);
+};
+
+ProgressListener.prototype.onStatusChange = function(
+ aWebProgress, aRequest, aStatus, aMessage) {
+ // TODO: Better figure out the possible aStatus values. Docs say:
+ // "This interface does not define the set of possible status codes."
+
+ // Manually found when reading an invalid file:/// URL.
+ if (2152857618 == aStatus) this._completionCallback(false);
+};
+
+/////////////////////////////// Public Interface ///////////////////////////////
+
+// Note: The design of this class is very asynchronous, with the result that
+// the code path spaghetti's through quite a few callbacks. A necessary evil.
+
+function RemoteScript(aUrl) {
+ this._baseName = null;
+ this._dependencies = [];
+ this._metadata = null;
+ this._progress = [0, 0];
+ this._progressCallbacks = [];
+ this._progressIndex = 0;
+ this._scriptFile = null;
+ this._scriptMetaCallbacks = [];
+ this._tempDir = GM_util.getTempDir();
+ this._uri = GM_util.uriFromUrl(aUrl);
+ this._url = aUrl;
+
+ this.done = false;
+ this.script = null;
+}
+
+/** Clean up all temporary files. */
+RemoteScript.prototype.cleanup = function() {
+ if (this._wbp) this._wbp.cancelSave();
+ if (this._tempDir && this._tempDir.exists()) {
+ this._tempDir.remove(true);
+ }
+};
+
+/** Download the entire script, starting from the .user.js itself. */
+RemoteScript.prototype.download = function(aCompletionCallback) {
+ aCompletionCallback = aCompletionCallback || function() {};
+ assertIsFunction(
+ aCompletionCallback, 'Completion callback is not a function.');
+
+ this.downloadScript(GM_util.hitch(this, function(aSuccess, aPoint) {
+ if (aSuccess) this._downloadDependencies(aCompletionCallback);
+ aCompletionCallback(aSuccess, aPoint);
+ }));
+};
+
+/** Download just enough of the script to find the metadata. */
+RemoteScript.prototype.downloadMetadata = function(aCallback) {
+ // TODO Is this good/useful? For update checking?
+};
+
+/** Download just the .user.js itself. Callback upon completion. */
+RemoteScript.prototype.downloadScript = function(aCompletionCallback) {
+ assertIsFunction(
+ aCompletionCallback, 'Completion callback is not a function.');
+ if (!this._url) throw Error('Tried to download script, but have no URL.');
+
+ this._scriptFile = GM_util.getTempFile(
+ this._tempDir, filenameFromUri(this._uri, 'gm_script'));
+
+ this._downloadFile(this._uri, this._scriptFile,
+ GM_util.hitch(this, this._downloadScriptCb, aCompletionCallback));
+};
+
+RemoteScript.prototype.install = function(aOldScript) {
+ if (!this.script) {
+ throw new Error('RemoteScript.install(): Script is not downloaded.');
+ }
+ if (!this._baseName) {
+ throw new Error('RemoteScript.install(): Script base name unknown.');
+ }
+
+ var suffix = 0;
+ var file = GM_util.scriptDir();
+ file.append(this._baseName);
+ while (file.exists()) {
+ suffix++;
+ file = GM_util.scriptDir();
+ file = append(this._baseName + '-' + suffix);
+ }
+ this._baseName = file.leafName;
+
+ this.script.setFilename(this._baseName, this._scriptFile.leafName);
+ this._tempDir.moveTo(GM_util.scriptDir(), this._baseName);
+ this._tempDir = null;
+
+ GM_config.install(this.script, aOldScript);
+};
+
+/** Add a progress callback. */
+RemoteScript.prototype.onProgress = function(aCallback) {
+ assertIsFunction(aCallback, 'Progress callback is not a function.');
+ this._progressCallbacks.push(aCallback);
+};
+
+/** Add a "script meta data is available" callback. */
+RemoteScript.prototype.onScriptMeta = function(aCallback) {
+ assertIsFunction(aCallback, 'Script meta callback is not a function.');
+ this._scriptMetaCallbacks.push(aCallback);
+};
+
+RemoteScript.prototype.toString = function() {
+ return '[RemoteScript object; ' + this._url + ']';
+};
+
+//////////////////////////// Private Implementation ////////////////////////////
+
+RemoteScript.prototype._dispatchCallbacks = function(aType, aData) {
+ var callbacks = this['_' + aType + 'Callbacks'];
+ if (!callbacks) {
+ throw Error('Invalid callback type: ' + aType);
+ }
+ for (var i = 0, callback = null; callback = callbacks[i]; i++) {
+ callback(this, aType, aData);
+ }
+};
+
+/** Download any dependencies (@icon, @require, @resource). */
+RemoteScript.prototype._downloadDependencies = function(aCompletionCallback) {
+ dump('>>> RemoteScript._downloadDependencies() ...\n');
+
+ this._progressIndex++;
+ if (this._progressIndex > this._dependencies.length) {
+ this.done = true;
+ this._dispatchCallbacks('progress', 1);
+ return aCompletionCallback(true, 'dependencies');
+ }
+
+ // Because _progressIndex includes the base script at 0, subtract one to
+ // get the dependency index.
+ var dependency = this._dependencies[this._progressIndex - 1];
+ var uri = GM_util.uriFromUrl(dependency.urlToDownload);
+ var file = GM_util.getTempFile(this._tempDir, filenameFromUri(uri));
+ dependency.setFilename(file);
+
+ function dependencyDownloadComplete(aChannel) {
+ if (dependency.setMimetype) {
+ dump('setting mime type for ' + dependency + ' to ' + aChannel.contentType + '\n');
+ dependency.setMimetype(aChannel.contentType);
+ }
+ if (dependency.setCharset) {
+ dump('setting charset for ' + dependency + ' to ' + aChannel.contentCharset + '\n');
+ dependency.setCharset(aChannel.contentCharset || null);
+ }
+ this._downloadDependencies(aCompletionCallback);
+ }
+
+ this._downloadFile(
+ uri, file, GM_util.hitch(this, dependencyDownloadComplete));
+};
+
+/** Download a given nsIURI to a given nsILocalFile, with optional callback. */
+RemoteScript.prototype._downloadFile = function(
+ aUri, aFile, aCompletionCallback) {
+ dump('>>> RemoteScript._downloadFile('+aUri.spec+') ...\n');
+ try {
+ aUri = aUri.QueryInterface(Ci.nsIURI);
+ aFile = aFile.QueryInterface(Ci.nsILocalFile);
+ aCompletionCallback = aCompletionCallback || function() {};
+ assertIsFunction(aCompletionCallback,
+ '_downloadFile() completion callback is not a function.');
+
+ // Dangerous semi-global state: The web browser persist object is stored
+ // in the object, so that it can be canceled. Parallel downloads would need
+ // to be handled differently.
+ var channel = ioService.newChannelFromURI(aUri);
+ this._wbp = Cc['@mozilla.org/embedding/browser/nsWebBrowserPersist;1']
+ .createInstance(Ci.nsIWebBrowserPersist);
+ this._wbp.persistFlags =
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_CLEANUP_ON_FAILURE |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_FORCE_ALLOW_COOKIES;
+ this._wbp.progressListener = new ProgressListener(
+ this,
+ GM_util.hitch(null, aCompletionCallback, channel),
+ GM_util.hitch(this, this._downloadFileProgress, channel));
+ this._wbp.saveChannel(channel, aFile);
+ } catch (e) { dump(e+'\n'); }
+};
+
+RemoteScript.prototype._htmlTypeRegex = new RegExp('^text/(x|ht)ml', 'i');
+RemoteScript.prototype._downloadFileProgress = function(
+ aChannel, aFileProgress) {
+ dump('>>> RemoteScript._downloadFileProgress('+aChannel+', '+aFileProgress+') ...\n');
+ if (0 == this._progressIndex && !this.script) {
+ // We are downloading the first file, and haven't parsed a script yet ...
+
+ // 1) Detect an HTML page and abort if so.
+ try {
+ // TODO: Detect the content type *after* 30x redirect.
+ var httpChannel = aChannel.QueryInterface(Ci.nsIHttpChannel);
+ var contentType = httpChannel.getResponseHeader('Content-Type');
+ if (this._htmlTypeRegex.test(contentType)) {
+ this.cleanup();
+ return false;
+ }
+ } catch (e) {
+ dump('RemoteScript._downloadFileProgress():\n\t' + e);
+ }
+
+ // 2) Otherwise try to parse the script from the downloaded file.
+ this.script = this._parseScriptFile();
+ if (this.script) {
+ // And if successful, prepare to download dependencies.
+ this._dependencies = this.script.requires.concat(this.script.resources);
+ if (this.script.icon.hasDownloadURL()) {
+ this._dependencies.push(this.script.icon);
+ }
+ this._progress = [];
+ for (var i = 0; i < this._dependencies.length; i++) {
+ this._progress[i] = 0;
+ }
+ }
+ }
+
+ this._progress[this._progressIndex] = aFileProgress;
+ var progress = this._progress.reduce(function(a, b) { return a + b; })
+ / this._progress.length;
+
+ this._dispatchCallbacks('progress', progress);
+
+ return true;
+};
+
+RemoteScript.prototype._downloadScriptCb = function(
+ aCompletionCallback, aChannel, aSuccess) {
+ dump('>>> RemoteScript._downloadScriptCb() ...\n');
+ if (aSuccess) {
+ // At this point downloading the script itself is definitely done.
+ if (!this.script) {
+ // If we don't have a script object, we failed to find metadata and parse
+ // it during download. Try one last time.
+ this.script = this._parseScriptFile(true);
+ }
+ if (!this.script) {
+ dump('RemoteScript: finishing with error because no script was found.\n');
+ // If we STILL don't have a script, this is a fatal error.
+ return aCompletionCallback(false, 'script');
+ }
+ } else {
+ this.cleanup();
+ }
+ aCompletionCallback(aSuccess, 'script');
+};
+
+/** Produce a Script object from the contents of this._scriptFile. */
+RemoteScript.prototype._metadataRegExp = new RegExp(
+ '^// ==UserScript==([\\s\\S]*?)^// ==/UserScript==', 'm');
+RemoteScript.prototype._parseScriptFile = function(aForce) {
+ var content = GM_util.getContents(this._scriptFile);
+ var meta = content.match(this._metadataRegExp);
+
+ var source = (meta && meta[0]) || (aForce && content) || null;
+ if (source) {
+ try {
+ var script = GM_config.parse(source, this._uri);
+ } catch (e) {
+ dump('RemoteScript._parseScriptFile error: ' + e + '\n');
+ // TODO: Surface this error? How?
+ // TODO: In case of parse error, stop download?
+ return null;
+ }
+ this._baseName = cleanFilename(script.name, 'gm-script');
+ this._dispatchCallbacks('scriptMeta', script);
+ return script;
+ }
+
+ return null;
+};
View
15 modules/util/getTempDir.js
@@ -0,0 +1,15 @@
+Components.utils.import('resource://greasemonkey/constants.js');
+
+const EXPORTED_SYMBOLS = ['getTempDir'];
+
+const DIRECTORY_TYPE = Components.interfaces.nsILocalFile.DIRECTORY_TYPE;
+const TMP_DIR = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("TmpD", Components.interfaces.nsILocalFile);
+
+function getTempDir(aRoot) {
+ var file = (aRoot || TMP_DIR).clone();
+ file.append("gm-temp");
+ file.createUnique(DIRECTORY_TYPE, GM_constants.directoryMask);
+ return file;
+}
View
6 modules/util/getTempFile.js
@@ -7,9 +7,9 @@ const TMP_DIR = Components.classes["@mozilla.org/file/directory_service;1"]
.getService(Components.interfaces.nsIProperties)
.get("TmpD", Components.interfaces.nsILocalFile);
-function getTempFile() {
- var file = TMP_DIR.clone();
- file.append("gm-temp");
+function getTempFile(aRoot, aLeaf) {
+ var file = (aRoot || TMP_DIR).clone();
+ file.append(aLeaf || 'gm-temp');
file.createUnique(NORMAL_FILE_TYPE, GM_constants.fileMask);
return file;
}

0 comments on commit 268f716

Please sign in to comment.
Something went wrong with that request. Please try again.