diff --git a/addon-sdk/moz.build b/addon-sdk/moz.build index 17e33c96beb5..837589dd20c1 100644 --- a/addon-sdk/moz.build +++ b/addon-sdk/moz.build @@ -9,8 +9,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -HAS_MISC_RULE = True - BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini'] JETPACK_ADDON_MANIFESTS += ['source/test/addons/jetpack-addon.ini'] @@ -21,7 +19,6 @@ EXTRA_JS_MODULES.sdk += [ EXTRA_JS_MODULES.sdk.system += [ 'source/modules/system/Startup.js', - 'source/modules/system/XulApp.js', ] if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk": @@ -39,6 +36,7 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk": 'source/lib/sdk/deprecated/list.js', 'source/lib/sdk/deprecated/memory.js', 'source/lib/sdk/deprecated/symbiont.js', + 'source/lib/sdk/deprecated/sync-worker.js', 'source/lib/sdk/deprecated/traits-worker.js', 'source/lib/sdk/deprecated/traits.js', 'source/lib/sdk/deprecated/unit-test-finder.js', @@ -54,7 +52,6 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk": EXTRA_JS_MODULES.commonjs.sdk.panel += [ 'source/lib/sdk/panel/events.js', 'source/lib/sdk/panel/utils.js', - 'source/lib/sdk/panel/window.js', ] EXTRA_JS_MODULES.commonjs.sdk.places += [ @@ -99,6 +96,7 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk": ] EXTRA_JS_MODULES.commonjs.sdk.ui += [ + 'source/lib/sdk/ui/component.js', 'source/lib/sdk/ui/frame.js', 'source/lib/sdk/ui/id.js', 'source/lib/sdk/ui/sidebar.js', @@ -133,13 +131,13 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk": 'source/lib/sdk/windows/dom.js', 'source/lib/sdk/windows/fennec.js', 'source/lib/sdk/windows/firefox.js', - 'source/lib/sdk/windows/loader.js', 'source/lib/sdk/windows/observer.js', 'source/lib/sdk/windows/tabs-fennec.js', 'source/lib/sdk/windows/tabs-firefox.js', ] EXTRA_JS_MODULES.commonjs += [ + 'source/lib/index.js', 'source/lib/test.js', ] @@ -173,10 +171,13 @@ EXTRA_JS_MODULES.commonjs.diffpatcher.test += [ ] EXTRA_JS_MODULES.commonjs.framescript += [ + 'source/lib/framescript/context-menu.js', 'source/lib/framescript/contextmenu-events.js', 'source/lib/framescript/FrameScriptManager.jsm', 'source/lib/framescript/LoaderHelper.jsm', + 'source/lib/framescript/manager.js', 'source/lib/framescript/tab-events.js', + 'source/lib/framescript/util.js', ] EXTRA_JS_MODULES.commonjs.method += [ @@ -191,6 +192,7 @@ EXTRA_JS_MODULES.commonjs.sdk += [ 'source/lib/sdk/base64.js', 'source/lib/sdk/clipboard.js', 'source/lib/sdk/context-menu.js', + 'source/lib/sdk/context-menu@2.js', 'source/lib/sdk/hotkeys.js', 'source/lib/sdk/indexed-db.js', 'source/lib/sdk/l10n.js', @@ -218,6 +220,7 @@ EXTRA_JS_MODULES.commonjs.sdk += [ ] EXTRA_JS_MODULES.commonjs.sdk.addon += [ + 'source/lib/sdk/addon/bootstrap.js', 'source/lib/sdk/addon/events.js', 'source/lib/sdk/addon/host.js', 'source/lib/sdk/addon/installer.js', @@ -246,10 +249,15 @@ EXTRA_JS_MODULES.commonjs.sdk.content += [ 'source/lib/sdk/content/thumbnail.js', 'source/lib/sdk/content/utils.js', 'source/lib/sdk/content/worker-child.js', - 'source/lib/sdk/content/worker-parent.js', 'source/lib/sdk/content/worker.js', ] +EXTRA_JS_MODULES.commonjs.sdk['context-menu'] += [ + 'source/lib/sdk/context-menu/context.js', + 'source/lib/sdk/context-menu/core.js', + 'source/lib/sdk/context-menu/readers.js', +] + EXTRA_JS_MODULES.commonjs.sdk.core += [ 'source/lib/sdk/core/disposable.js', 'source/lib/sdk/core/heritage.js', @@ -395,6 +403,7 @@ EXTRA_JS_MODULES.commonjs.sdk.system += [ 'source/lib/sdk/system/runtime.js', 'source/lib/sdk/system/unload.js', 'source/lib/sdk/system/xul-app.js', + 'source/lib/sdk/system/xul-app.jsm', ] EXTRA_JS_MODULES.commonjs.sdk.system.child_process += [ @@ -426,12 +435,17 @@ EXTRA_JS_MODULES.commonjs.sdk.ui.toolbar += [ 'source/lib/sdk/ui/toolbar/view.js', ] +EXTRA_JS_MODULES.commonjs.sdk.uri += [ + 'source/lib/sdk/uri/resource.js', +] + EXTRA_JS_MODULES.commonjs.sdk.url += [ 'source/lib/sdk/url/utils.js', ] EXTRA_JS_MODULES.commonjs.sdk.util += [ 'source/lib/sdk/util/array.js', + 'source/lib/sdk/util/bond.js', 'source/lib/sdk/util/collection.js', 'source/lib/sdk/util/contract.js', 'source/lib/sdk/util/deprecate.js', @@ -439,7 +453,6 @@ EXTRA_JS_MODULES.commonjs.sdk.util += [ 'source/lib/sdk/util/list.js', 'source/lib/sdk/util/match-pattern.js', 'source/lib/sdk/util/object.js', - 'source/lib/sdk/util/registry.js', 'source/lib/sdk/util/rules.js', 'source/lib/sdk/util/sequence.js', 'source/lib/sdk/util/uuid.js', diff --git a/addon-sdk/mozbuild.template b/addon-sdk/mozbuild.template index 174593e33d76..6828be17a403 100644 --- a/addon-sdk/mozbuild.template +++ b/addon-sdk/mozbuild.template @@ -14,5 +14,4 @@ EXTRA_JS_MODULES.sdk += [ EXTRA_JS_MODULES.sdk.system += [ 'source/modules/system/Startup.js', - 'source/modules/system/XulApp.js', ] diff --git a/addon-sdk/source/.gitignore b/addon-sdk/source/.gitignore index 38a7e212f639..ac77a32ac083 100644 --- a/addon-sdk/source/.gitignore +++ b/addon-sdk/source/.gitignore @@ -8,6 +8,7 @@ doc/index.html doc/modules/ doc/status.md5 packages/* +node_modules # Python *.pyc @@ -17,4 +18,3 @@ packages/* # Windows *Thumbs.db - diff --git a/addon-sdk/source/.hgignore b/addon-sdk/source/.hgignore index 27bf933c738e..05ac424d4dfb 100644 --- a/addon-sdk/source/.hgignore +++ b/addon-sdk/source/.hgignore @@ -5,6 +5,7 @@ testdocs.tgz jetpack-sdk-docs.tgz .test_tmp jetpack-sdk-docs +node_modules # These should really be in a global .hgignore, but such a thing # seems ridiculously confusing to set up, so we'll include some diff --git a/addon-sdk/source/.jpmignore b/addon-sdk/source/.jpmignore new file mode 100644 index 000000000000..9ec100e02a8c --- /dev/null +++ b/addon-sdk/source/.jpmignore @@ -0,0 +1,17 @@ +local.json +mapping.json +CONTRIBUTING.md +@addon-sdk.xpi +.* +app-extension/ +bin/ +modules/ +node_modules/ +examples/ + +# Python +python-lib/ +*.pyc + +# Windows +*Thumbs.db diff --git a/addon-sdk/source/.travis.yml b/addon-sdk/source/.travis.yml new file mode 100644 index 000000000000..fe5c1b7a1145 --- /dev/null +++ b/addon-sdk/source/.travis.yml @@ -0,0 +1,23 @@ +sudo: false +language: node_js +node_js: + - "0.10" + +notifications: + irc: "irc.mozilla.org#jetpack" + +before_install: + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" + - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 -extension RANDR" + +before_script: + - npm install mozilla-download -g + - npm install jpm -g + - cd .. + - mozilla-download --branch nightly -c prerelease --host ftp.mozilla.org firefox + - export JPM_FIREFOX_BINARY=$TRAVIS_BUILD_DIR/../firefox/firefox + - cd $TRAVIS_BUILD_DIR + +script: + - npm test diff --git a/addon-sdk/source/README b/addon-sdk/source/README deleted file mode 100644 index 8c1c360e2930..000000000000 --- a/addon-sdk/source/README +++ /dev/null @@ -1,41 +0,0 @@ -Add-on SDK README -================== - -Before proceeding, please make sure you've installed Python 2.5, -2.6, or 2.7 (if it's not already on your system): - - http://python.org/download/ - -Note that Python 3 is not supported. - -For Windows users, MozillaBuild (https://wiki.mozilla.org/MozillaBuild) -will install the correct version of Python and the MSYS package, which -will make it easier to work with the SDK. - -To get started, first enter the same directory that this README file -is in (the SDK's root directory) using a shell program. On Unix systems -or on Windows with MSYS, you can execute the following command: - - source bin/activate - -Windows users using cmd.exe should instead run: - - bin\activate.bat - -Then go to https://developer.mozilla.org/en-US/Add-ons/SDK/ -to browse the SDK documentation. - -If you get an error when running cfx or have any other problems getting -started, see the "Troubleshooting" guide at: -https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Troubleshooting - -Bugs -------- - -* file a bug: https://bugzilla.mozilla.org/enter_bug.cgi?product=Add-on%20SDK - - -Style Guidelines --------------------- - -* https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide diff --git a/addon-sdk/source/README.md b/addon-sdk/source/README.md new file mode 100644 index 000000000000..43d796aea636 --- /dev/null +++ b/addon-sdk/source/README.md @@ -0,0 +1,31 @@ +# Mozilla Add-on SDK [![Build Status](https://travis-ci.org/mozilla/addon-sdk.png)](https://travis-ci.org/mozilla/addon-sdk) + +Using the Add-on SDK you can create Firefox add-ons using standard Web technologies: JavaScript, HTML, and CSS. The SDK includes JavaScript APIs which you can use to create add-ons, and tools for creating, running, testing, and packaging add-ons. + +If you find a problem, please [report the bug here](https://bugzilla.mozilla.org/enter_bug.cgi?product=Add-on%20SDK). + +## Developing Add-ons + +These resources should provide some help: + +* [Add-on SDK Documentation](https://developer.mozilla.org/en-US/Add-ons/SDK) +* [Community Developed Modules](https://github.com/mozilla/addon-sdk/wiki/Community-developed-modules) +* [Jetpack FAQ](https://wiki.mozilla.org/Jetpack/FAQ) +* [StackOverflow Questions](http://stackoverflow.com/questions/tagged/firefox-addon-sdk) +* [Mailing List](https://wiki.mozilla.org/Jetpack#Mailing_list) +* #jetpack on irc.mozilla.org + +## Contributing Code + +Please read these two guides if you wish to contribute some patches to the addon-sdk: + +* [Contribute Guide](https://github.com/mozilla/addon-sdk/wiki/Contribute) +* [Style Guide](https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide) + +## Issues + +We use [bugzilla](https://bugzilla.mozilla.org/) as our issue tracker, here are some useful links: + +* [File a bug](https://bugzilla.mozilla.org/enter_bug.cgi?product=Add-on%20SDK) +* [Open bugs](https://bugzilla.mozilla.org/buglist.cgi?bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&columnlist=bug_severity%2Cpriority%2Cassigned_to%2Cbug_status%2Ctarget_milestone%2Cresolution%2Cshort_desc%2Cchangeddate&product=Add-on%20SDK&query_format=advanced&order=priority) +* [Good first bugs](https://bugzilla.mozilla.org/buglist.cgi?status_whiteboard=[good+first+bug]&&resolution=---&product=Add-on+SDK) diff --git a/addon-sdk/source/bin/jpm-test.js b/addon-sdk/source/bin/jpm-test.js new file mode 100644 index 000000000000..0a9c113728f9 --- /dev/null +++ b/addon-sdk/source/bin/jpm-test.js @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +var BLACKLIST = []; +var readParam = require("./node-scripts/utils").readParam; +var path = require("path"); +var Mocha = require("mocha"); +var mocha = new Mocha({ + ui: "bdd", + reporter: "spec", + timeout: 900000 +}); + +var type = readParam("type"); + +[ + (!type || type == "modules") && require.resolve("../bin/node-scripts/test.modules"), + (!type || type == "addons") && require.resolve("../bin/node-scripts/test.addons"), + (!type || type == "examples") && require.resolve("../bin/node-scripts/test.examples"), +].sort().forEach(function(filepath) { + filepath && mocha.addFile(filepath); +}) + +mocha.run(function (failures) { + process.exit(failures); +}); diff --git a/addon-sdk/source/bin/node-scripts/test.addons.js b/addon-sdk/source/bin/node-scripts/test.addons.js new file mode 100644 index 000000000000..27a3349ffe4d --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/test.addons.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +var utils = require("./utils"); +var path = require("path"); +var fs = require("fs"); +var jpm = utils.run; +var readParam = utils.readParam; + +var addonsPath = path.join(__dirname, "..", "..", "test", "addons"); + +var binary = process.env.JPM_FIREFOX_BINARY || "nightly"; +var filterPattern = readParam("filter"); + +describe("jpm test sdk addons", function () { + fs.readdirSync(addonsPath) + .filter(fileFilter.bind(null, addonsPath)) + .forEach(function (file) { + it(file, function (done) { + var addonPath = path.join(addonsPath, file); + process.chdir(addonPath); + + var options = { cwd: addonPath, env: { JPM_FIREFOX_BINARY: binary }}; + if (process.env.DISPLAY) { + options.env.DISPLAY = process.env.DISPLAY; + } + if (/^e10s/.test(file)) { + options.e10s = true; + } + + jpm("run", options).then(done).catch(done); + }); + }); +}); + +function fileFilter(root, file) { + var matcher = filterPattern && new RegExp(filterPattern); + if (/^(l10n|simple-prefs|page-mod-debugger)/.test(file)) { + return false; + } + if (matcher && !matcher.test(file)) { + return false; + } + var stat = fs.statSync(path.join(root, file)) + return (stat && stat.isDirectory()); +} diff --git a/addon-sdk/source/bin/node-scripts/test.examples.js b/addon-sdk/source/bin/node-scripts/test.examples.js new file mode 100644 index 000000000000..71f7ee43c117 --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/test.examples.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +var utils = require("./utils"); +var path = require("path"); +var fs = require("fs"); +var jpm = utils.run; +var readParam = utils.readParam; + +var examplesPath = path.join(__dirname, "..", "..", "examples"); + +var binary = process.env.JPM_FIREFOX_BINARY || "nightly"; +var filterPattern = readParam("filter"); + +describe("jpm test sdk examples", function () { + fs.readdirSync(examplesPath) + .filter(fileFilter.bind(null, examplesPath)) + .forEach(function (file) { + it(file, function (done) { + var addonPath = path.join(examplesPath, file); + process.chdir(addonPath); + + var options = { cwd: addonPath, env: { JPM_FIREFOX_BINARY: binary }}; + if (process.env.DISPLAY) { + options.env.DISPLAY = process.env.DISPLAY; + } + + jpm("test", options).then(done); + }); + }); +}); + +function fileFilter(root, file) { + var matcher = filterPattern && new RegExp(filterPattern); + if (/^(reading-data)/.test(file)) { + return false; + } + if (matcher && !matcher.test(file)) { + return false; + } + var stat = fs.statSync(path.join(root, file)) + return (stat && stat.isDirectory()); +} diff --git a/addon-sdk/source/bin/node-scripts/test.modules.js b/addon-sdk/source/bin/node-scripts/test.modules.js new file mode 100644 index 000000000000..eb400a5f3d37 --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/test.modules.js @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +var utils = require("./utils"); +var readParam = utils.readParam; +var path = require("path"); +var fs = require("fs"); +var jpm = utils.run; +var sdk = path.join(__dirname, "..", ".."); +var binary = process.env.JPM_FIREFOX_BINARY || "nightly"; + +var filterPattern = readParam("filter"); + +describe("jpm test sdk modules", function () { + it("SDK Modules", function (done) { + process.chdir(sdk); + + var options = { cwd: sdk, env: { JPM_FIREFOX_BINARY: binary } }; + if (process.env.DISPLAY) { + options.env.DISPLAY = process.env.DISPLAY; + } + options.filter = filterPattern; + + jpm("test", options, process).then(done); + }); +}); diff --git a/addon-sdk/source/bin/node-scripts/utils.js b/addon-sdk/source/bin/node-scripts/utils.js new file mode 100644 index 000000000000..bd7c68b36a69 --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/utils.js @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +var _ = require("lodash"); +var path = require("path"); +var child_process = require("child_process"); +var jpm = require.resolve("../../node_modules/jpm/bin/jpm"); +var Promise = require("promise"); +var chai = require("chai"); +var expect = chai.expect; +var assert = chai.assert; +var DEFAULT_PROCESS = process; + +var sdk = path.join(__dirname, "..", ".."); +var prefsPath = path.join(sdk, "test", "preferences", "test-preferences.js"); +var e10sPrefsPath = path.join(sdk, "test", "preferences", "test-e10s-preferences.js"); + +function spawn (cmd, options) { + options = options || {}; + var env = _.extend({}, options.env, process.env); + var e10s = options.e10s || false; + + return child_process.spawn("node", [ + jpm, cmd, "-v", + "--prefs", e10s ? e10sPrefsPath : prefsPath, + "-o", sdk, + "-f", options.filter || "" + ], { + cwd: options.cwd || tmpOutputDir, + env: env + }); +} +exports.spawn = spawn; + +function run (cmd, options, p) { + return new Promise(function(resolve) { + var output = []; + var proc = spawn(cmd, options); + proc.stderr.pipe(process.stderr); + proc.stdout.on("data", function (data) { + output.push(data); + }); + if (p) { + proc.stdout.pipe(p.stdout); + } + proc.on("close", function(code) { + var out = output.join(""); + var noTests = /No tests were run/.test(out); + var hasSuccess = /All tests passed!/.test(out); + var hasFailure = /There were test failures\.\.\./.test(out); + if (noTests || hasFailure || !hasSuccess || code != 0) { + DEFAULT_PROCESS.stdout.write(out); + } + expect(code).to.equal(hasFailure ? 1 : 0); + expect(hasFailure).to.equal(false); + expect(hasSuccess).to.equal(true); + expect(noTests).to.equal(false); + resolve(); + }); + }); +} +exports.run = run; + +function readParam(name) { + var index = process.argv.indexOf("--" + name) + return index >= 0 && process.argv[index + 1] +} +exports.readParam = readParam; diff --git a/addon-sdk/source/bootstrap.js b/addon-sdk/source/bootstrap.js new file mode 100644 index 000000000000..cb9837b3c62c --- /dev/null +++ b/addon-sdk/source/bootstrap.js @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Note that this file is temporary workaroud until JPM is smart enough +// to cover it on it's own. + +const { utils: Cu } = Components; +const rootURI = __SCRIPT_URI_SPEC__.replace("bootstrap.js", ""); +const { require } = Cu.import(`${rootURI}/lib/toolkit/require.js`, {}); +const { Bootstrap } = require(`${rootURI}/lib/sdk/addon/bootstrap.js`); +const { startup, shutdown, install, uninstall } = new Bootstrap(rootURI); diff --git a/addon-sdk/source/examples/actor-repl/test/test-main.js b/addon-sdk/source/examples/actor-repl/test/test-main.js new file mode 100644 index 000000000000..9862fc20b78d --- /dev/null +++ b/addon-sdk/source/examples/actor-repl/test/test-main.js @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + "use strict"; + +exports.testMain = function(assert) { + assert.pass("TODO: Write some tests."); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/examples/annotator/package.json b/addon-sdk/source/examples/annotator/package.json index 3870710cd18c..da0535de9656 100644 --- a/addon-sdk/source/examples/annotator/package.json +++ b/addon-sdk/source/examples/annotator/package.json @@ -1,9 +1,11 @@ { - "license": "MPL 2.0", - "name": "annotator", - "contributors": [], - "author": "Will Bamberg", - "keywords": [], - "id": "anonid0-annotator", - "description": "Add notes to Web pages" + "license": "MPL 2.0", + "name": "annotator", + "contributors": [], + "author": "Will Bamberg", + "keywords": [], + "version": "0.1.1", + "id": "anonid0-annotator@jetpack", + "description": "Add notes to Web pages", + "main": "./lib/main.js" } diff --git a/addon-sdk/source/examples/annotator/tests/test-main.js b/addon-sdk/source/examples/annotator/tests/test-main.js index 72fedf45cc24..9862fc20b78d 100644 --- a/addon-sdk/source/examples/annotator/tests/test-main.js +++ b/addon-sdk/source/examples/annotator/tests/test-main.js @@ -1,7 +1,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + "use strict"; -exports.testMain = function(test) { - test.pass("TODO: Write some tests."); +exports.testMain = function(assert) { + assert.pass("TODO: Write some tests."); }; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/examples/debug-client/test/test-main.js b/addon-sdk/source/examples/debug-client/test/test-main.js new file mode 100644 index 000000000000..9862fc20b78d --- /dev/null +++ b/addon-sdk/source/examples/debug-client/test/test-main.js @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + "use strict"; + +exports.testMain = function(assert) { + assert.pass("TODO: Write some tests."); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/examples/library-detector/package.json b/addon-sdk/source/examples/library-detector/package.json index 60cce53025af..d50b0e466e74 100755 --- a/addon-sdk/source/examples/library-detector/package.json +++ b/addon-sdk/source/examples/library-detector/package.json @@ -1,9 +1,10 @@ { - "name": "library-detector-sdk", - "license": "MPL 2.0", - "author": "", - "version": "0.1", - "title": "library-detector-sdk", - "id": "jid1-R4rSVNkBANnvGQ", - "description": "a basic add-on" + "name": "library-detector-sdk", + "license": "MPL 2.0", + "author": "", + "version": "0.1.1", + "title": "library-detector-sdk", + "id": "jid1-R4rSVNkBANnvGQ@jetpack", + "description": "a basic add-on", + "main": "./lib/main.js" } diff --git a/addon-sdk/source/examples/library-detector/test/test-main.js b/addon-sdk/source/examples/library-detector/test/test-main.js index 72fedf45cc24..9862fc20b78d 100644 --- a/addon-sdk/source/examples/library-detector/test/test-main.js +++ b/addon-sdk/source/examples/library-detector/test/test-main.js @@ -1,7 +1,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + "use strict"; -exports.testMain = function(test) { - test.pass("TODO: Write some tests."); +exports.testMain = function(assert) { + assert.pass("TODO: Write some tests."); }; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/examples/toolbar-api/package.json b/addon-sdk/source/examples/toolbar-api/package.json index dbceabb99f1a..0ce367af0974 100644 --- a/addon-sdk/source/examples/toolbar-api/package.json +++ b/addon-sdk/source/examples/toolbar-api/package.json @@ -5,7 +5,7 @@ "description": "a toolbar api example", "author": "", "license": "MPL 2.0", - "version": "0.1", + "version": "0.1.1", "engines": { "firefox": ">=27.0 <=30.0" } diff --git a/addon-sdk/source/examples/toolbar-api/test/test-main.js b/addon-sdk/source/examples/toolbar-api/test/test-main.js new file mode 100644 index 000000000000..9862fc20b78d --- /dev/null +++ b/addon-sdk/source/examples/toolbar-api/test/test-main.js @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + "use strict"; + +exports.testMain = function(assert) { + assert.pass("TODO: Write some tests."); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/examples/ui-button-apis/package.json b/addon-sdk/source/examples/ui-button-apis/package.json index 2905031e4173..f4f7906c23dd 100644 --- a/addon-sdk/source/examples/ui-button-apis/package.json +++ b/addon-sdk/source/examples/ui-button-apis/package.json @@ -5,5 +5,6 @@ "description": "A Button API example", "author": "jeff@canuckistani.ca (Jeff Griffiths | @canuckistani)", "license": "MPL 2.0", - "version": "0.1" + "version": "0.1.1", + "main": "./lib/main.js" } diff --git a/addon-sdk/source/examples/ui-button-apis/tests/test-main.js b/addon-sdk/source/examples/ui-button-apis/tests/test-main.js index 88ece12b6857..49bdc863a8dd 100644 --- a/addon-sdk/source/examples/ui-button-apis/tests/test-main.js +++ b/addon-sdk/source/examples/ui-button-apis/tests/test-main.js @@ -3,7 +3,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -var { actionButton, toggleButton, icon } = require("main"); +try { + // CFX use case.. + var { actionButton, toggleButton, icon } = require("main"); +} +catch (e) { + // JPM use case.. + let mainURI = "../lib/main"; + var { actionButton, toggleButton, icon } = require(mainURI); +} var self = require("sdk/self"); exports.testActionButton = function(assert) { diff --git a/addon-sdk/source/lib/dev/utils.js b/addon-sdk/source/lib/dev/utils.js index f01c6fed83d0..35522b3383a5 100644 --- a/addon-sdk/source/lib/dev/utils.js +++ b/addon-sdk/source/lib/dev/utils.js @@ -16,11 +16,13 @@ const targetFor = target => { return devtools.TargetFactory.forTab(target); }; +const getId = id => ((id.prototype && id.prototype.id) || id.id || id); + const getCurrentPanel = toolbox => toolbox.getCurrentPanel(); exports.getCurrentPanel = getCurrentPanel; const openToolbox = (id, tab) => { - id = id.prototype.id || id.id || id; + id = getId(id); return gDevTools.showToolbox(targetFor(tab), id); }; exports.openToolbox = openToolbox; @@ -32,7 +34,7 @@ const getToolbox = tab => gDevTools.getToolbox(targetFor(tab)); exports.getToolbox = getToolbox; const openToolboxPanel = (id, tab) => { - id = id.prototype.id || id.id || id; + id = getId(id); return gDevTools.showToolbox(targetFor(tab), id).then(getCurrentPanel); }; exports.openToolboxPanel = openToolboxPanel; diff --git a/addon-sdk/source/lib/dev/volcan.js b/addon-sdk/source/lib/dev/volcan.js index 62b4b6032780..bbd73a1f1026 100644 --- a/addon-sdk/source/lib/dev/volcan.js +++ b/addon-sdk/source/lib/dev/volcan.js @@ -1,3 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ !function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.volcan=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o (...args) => { + try { + return fn(...args); + } catch(error) { + console.error(error); + return fallback; + } +}; + +// Decorator funciton that takes `f` function and returns one that returns +// JSON cloned result of whatever `f` returns for given arguments. +const JSONReturn = f => (...args) => JSON.parse(JSON.stringify(f(...args))); + +const Null = constant(null); + +// Table of readers mapped to field names they're going to be reading. +const readers = Object.create(null); +// Read function takes "contextmenu" event target `node` and returns table of +// read field names mapped to appropriate values. Read uses above defined read +// table to read data for all registered readers. +const read = node => + object(...map(([id, read]) => [id, read(node, id)], pairs(readers))); + +// Table of built-in readers, each takes a descriptor and returns a reader: +// descriptor -> node -> JSON +const parsers = Object.create(null) +// Function takes a descriptor of the remotely defined reader and parsese it +// to construct a local reader that's going to read out data from context menu +// target. +const parse = descriptor => { + const parser = parsers[descriptor.category]; + if (!parser) { + console.error("Unknown reader descriptor was received", descriptor, `"${descriptor.category}"`); + return Null + } + return Try(parser(descriptor)); +} + +// TODO: Test how chrome's mediaType behaves to try and match it's behavior. +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const SVG_NS = "http://www.w3.org/2000/svg"; + +// Firefox always creates a HTMLVideoElement when loading an ogg file +// directly. If the media is actually audio, be smarter and provide a +// context menu with audio operations. +// Source: https://github.com/mozilla/gecko-dev/blob/28c2fca3753c5371643843fc2f2f205146b083b7/browser/base/content/nsContextMenu.js#L632-L637 +const isVideoLoadingAudio = node => + node.readyState >= node.HAVE_METADATA && + (node.videoWidth == 0 || node.videoHeight == 0) + +const isVideo = node => + node instanceof node.ownerDocument.defaultView.HTMLVideoElement && + !isVideoLoadingAudio(node); + +const isAudio = node => { + const {HTMLVideoElement, HTMLAudioElement} = node.ownerDocument.defaultView; + return node instanceof HTMLAudioElement ? true : + node instanceof HTMLVideoElement ? isVideoLoadingAudio(node) : + false; +}; + +const isImage = ({namespaceURI, localName}) => + namespaceURI === HTML_NS && localName === "img" ? true : + namespaceURI === XUL_NS && localName === "image" ? true : + namespaceURI === SVG_NS && localName === "image" ? true : + false; + +parsers["reader/MediaType()"] = constant(node => + isImage(node) ? "image" : + isAudio(node) ? "audio" : + isVideo(node) ? "video" : + null); + + +const readLink = node => + node.namespaceURI === HTML_NS && node.localName === "a" ? node.href : + readLink(node.parentNode); + +parsers["reader/LinkURL()"] = constant(node => + node.matches("a, a *") ? readLink(node) : null); + +// Reader that reads out `true` if "contextmenu" `event.target` matches +// `descriptor.selector` and `false` if it does not. +parsers["reader/SelectorMatch()"] = ({selector}) => + node => node.matches(selector); + +// Accessing `selectionStart` and `selectionEnd` properties on non +// editable input nodes throw exceptions, there for we need this util +// function to guard us against them. +const getInputSelection = node => { + try { + if ("selectionStart" in node && "selectionEnd" in node) { + const {selectionStart, selectionEnd} = node; + return {selectionStart, selectionEnd} + } + } + catch(_) {} + + return null; +} + +// Selection reader does not really cares about descriptor so it is +// a constant function returning selection reader. Selection reader +// returns string of the selected text or `null` if there is no selection. +parsers["reader/Selection()"] = constant(node => { + const selection = node.ownerDocument.getSelection(); + if (!selection.isCollapsed) { + return selection.toString(); + } + // If target node is editable (text, input, textarea, etc..) document does + // not really handles selections there. There for we fallback to checking + // `selectionStart` `selectionEnd` properties and if they are present we + // extract selections manually from the `node.value`. + else { + const selection = getInputSelection(node); + const isSelected = selection && + Number.isInteger(selection.selectionStart) && + Number.isInteger(selection.selectionEnd) && + selection.selectionStart !== selection.selectionEnd; + return isSelected ? node.value.substring(selection.selectionStart, + selection.selectionEnd) : + null; + } +}); + +// Query reader just reads out properties from the node, so we just use `query` +// utility function. +parsers["reader/Query()"] = ({path}) => JSONReturn(query(path)); +// Attribute reader just reads attribute of the event target node. +parsers["reader/Attribute()"] = ({name}) => node => node.getAttribute(name); + +// Extractor reader defines generates a reader out of serialized function, who's +// return value is JSON cloned. Note: We do know source will evaluate to function +// as that's what we serialized on the other end, it's also ok if generated function +// is going to throw as registered readers are wrapped in try catch to avoid breakting +// unrelated readers. +parsers["reader/Extractor()"] = ({source}) => + JSONReturn(new Function("return (" + source + ")")()); + +// If the context-menu target node or any of its ancestors is one of these, +// Firefox uses a tailored context menu, and so the page context doesn't apply. +// There for `reader/isPage()` will read `false` in that case otherwise it's going +// to read `true`. +const nonPageElements = ["a", "applet", "area", "button", "canvas", "object", + "embed", "img", "input", "map", "video", "audio", "menu", + "option", "select", "textarea", "[contenteditable=true]"]; +const nonPageSelector = nonPageElements. + concat(nonPageElements.map(tag => `${tag} *`)). + join(", "); + +// Note: isPageContext implementation could have actually used SelectorMatch reader, +// but old implementation was also checked for collapsed selection there for to keep +// the behavior same we end up implementing a new reader. +parsers["reader/isPage()"] = constant(node => + node.ownerDocument.defaultView.getSelection().isCollapsed && + !node.matches(nonPageSelector)); + +// Reads `true` if node is in an iframe otherwise returns true. +parsers["reader/isFrame()"] = constant(node => + !!node.ownerDocument.defaultView.frameElement); + +parsers["reader/isEditable()"] = constant(node => { + const selection = getInputSelection(node); + return selection ? !node.readOnly && !node.disabled : node.isContentEditable; +}); + + +// TODO: Add some reader to read out tab id. + +const onReadersUpdate = message => { + each(([id, descriptor]) => { + if (descriptor) { + readers[id] = parse(descriptor); + } + else { + delete readers[id]; + } + }, pairs(message.data)); +}; +exports.onReadersUpdate = onReadersUpdate; + + +const onContextMenu = event => { + if (!event.defaultPrevented) { + const manager = nodeToMessageManager(event.target); + manager.sendSyncMessage("sdk/context-menu/read", read(event.target), readers); + } +}; +exports.onContextMenu = onContextMenu; + + +const onContentFrame = (frame) => { + // Listen for contextmenu events in on this frame. + frame.addEventListener("contextmenu", onContextMenu); + // Listen to registered reader changes and update registry. + frame.addMessageListener("sdk/context-menu/readers", onReadersUpdate); + + // Request table of readers (if this is loaded in a new process some table + // changes may be missed, this is way to sync up). + frame.sendAsyncMessage("sdk/context-menu/readers?"); +}; +exports.onContentFrame = onContentFrame; diff --git a/addon-sdk/source/lib/framescript/manager.js b/addon-sdk/source/lib/framescript/manager.js new file mode 100644 index 000000000000..1f261e1fa54a --- /dev/null +++ b/addon-sdk/source/lib/framescript/manager.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const mime = "application/javascript"; +const requireURI = module.uri.replace("framescript/manager.js", + "toolkit/require.js"); + +const requireLoadURI = `data:${mime},this["Components"].utils.import("${requireURI}")` + +// Loads module with given `id` into given `messageManager` via shared module loader. If `init` +// string is passed, will call module export with that name and pass frame script environment +// of the `messageManager` into it. Since module will load only once per process (which is +// once for chrome proces & second for content process) it is useful to have an init function +// to setup event listeners on each content frame. +const loadModule = (messageManager, id, allowDelayed, init) => { + const moduleLoadURI = `${requireLoadURI}.require("${id}")` + const uri = init ? `${moduleLoadURI}.${init}(this)` : moduleLoadURI; + messageManager.loadFrameScript(uri, allowDelayed); +}; +exports.loadModule = loadModule; diff --git a/addon-sdk/source/lib/framescript/util.js b/addon-sdk/source/lib/framescript/util.js new file mode 100644 index 000000000000..fb6834608a77 --- /dev/null +++ b/addon-sdk/source/lib/framescript/util.js @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + + +const { Ci } = require("chrome"); + +const windowToMessageManager = window => + window. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDocShell). + sameTypeRootTreeItem. + QueryInterface(Ci.nsIDocShell). + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIContentFrameMessageManager); +exports.windowToMessageManager = windowToMessageManager; + +const nodeToMessageManager = node => + windowToMessageManager(node.ownerDocument.defaultView); +exports.nodeToMessageManager = nodeToMessageManager; diff --git a/addon-sdk/source/lib/index.js b/addon-sdk/source/lib/index.js new file mode 100644 index 000000000000..e0032240a4d2 --- /dev/null +++ b/addon-sdk/source/lib/index.js @@ -0,0 +1,3 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ diff --git a/addon-sdk/source/lib/sdk/addon/bootstrap.js b/addon-sdk/source/lib/sdk/addon/bootstrap.js new file mode 100644 index 000000000000..1a4544073393 --- /dev/null +++ b/addon-sdk/source/lib/sdk/addon/bootstrap.js @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Cu } = require("chrome"); +const { NetUtil } = require("resource://gre/modules/NetUtil.jsm"); +const { Task: { spawn } } = require("resource://gre/modules/Task.jsm"); +const { readURI } = require("sdk/net/url"); +const { mount, unmount } = require("sdk/uri/resource"); +const { setTimeout } = require("sdk/timers"); +const { Loader, Require, Module, main, unload } = require("toolkit/loader"); +const prefs = require("sdk/preferences/service"); + +// load below now, so that it can be used by sdk/addon/runner +// see bug https://bugzilla.mozilla.org/show_bug.cgi?id=1042239 +const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}); + +const REASON = [ "unknown", "startup", "shutdown", "enable", "disable", + "install", "uninstall", "upgrade", "downgrade" ]; + +const UUID_PATTERN = /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/; +// Takes add-on ID and normalizes it to a domain name so that add-on +// can be mapped to resource://domain/ +const readDomain = id => + // If only `@` character is the first one, than just substract it, + // otherwise fallback to legacy normalization code path. Note: `.` + // is valid character for resource substitutaiton & we intend to + // make add-on URIs intuitive, so it's best to just stick to an + // add-on author typed input. + id.lastIndexOf("@") === 0 ? id.substr(1).toLowerCase() : + id.toLowerCase(). + replace(/@/g, "-at-"). + replace(/\./g, "-dot-"). + replace(UUID_PATTERN, "$1"); + +const readPaths = id => { + const base = `extensions.modules.${id}.path.`; + const domain = readDomain(id); + return prefs.keys(base).reduce((paths, key) => { + const value = prefs.get(key); + const name = key.replace(base, ""); + const path = name.split(".").join("/"); + const prefix = path.length ? `${path}/` : path; + const uri = value.endsWith("/") ? value : `${value}/`; + const root = `extensions.modules.${domain}.commonjs.path.${name}`; + + mount(root, uri); + + paths[prefix] = `resource://${root}/`; + return paths; + }, {}); +}; + +const Bootstrap = function(mountURI) { + this.mountURI = mountURI; + this.install = this.install.bind(this); + this.uninstall = this.uninstall.bind(this); + this.startup = this.startup.bind(this); + this.shutdown = this.shutdown.bind(this); +}; +Bootstrap.prototype = { + constructor: Bootstrap, + mount(domain, rootURI) { + mount(domain, rootURI); + this.domain = domain; + }, + unmount() { + if (this.domain) { + unmount(this.domain); + this.domain = null; + } + }, + install(addon, reason) { + }, + uninstall(addon, reason) { + const {id} = addon; + + prefs.reset(`extensions.${id}.sdk.domain`); + prefs.reset(`extensions.${id}.sdk.version`); + prefs.reset(`extensions.${id}.sdk.rootURI`); + prefs.reset(`extensions.${id}.sdk.baseURI`); + prefs.reset(`extensions.${id}.sdk.load.reason`); + + }, + startup(addon, reasonCode) { + const { id, version, resourceURI: {spec: addonURI} } = addon; + const rootURI = this.mountURI || addonURI; + const reason = REASON[reasonCode]; + + spawn(function*() { + const metadata = JSON.parse(yield readURI(`${rootURI}package.json`)); + const domain = readDomain(id); + const baseURI = `resource://${domain}/`; + + this.mount(domain, rootURI); + + prefs.set(`extensions.${id}.sdk.domain`, domain); + prefs.set(`extensions.${id}.sdk.version`, version); + prefs.set(`extensions.${id}.sdk.rootURI`, rootURI); + prefs.set(`extensions.${id}.sdk.baseURI`, baseURI); + prefs.set(`extensions.${id}.sdk.load.reason`, reason); + + const command = prefs.get(`extensions.${id}.sdk.load.command`); + + const loader = Loader({ + id, + isNative: true, + checkCompatibility: true, + prefixURI: baseURI, + rootURI: baseURI, + name: metadata.name, + paths: Object.assign({ + "": "resource://gre/modules/commonjs/", + "devtools/": "resource://gre/modules/devtools/", + "./": baseURI + }, readPaths(id)), + manifest: metadata, + metadata: metadata, + modules: { + "@test/options": {} + }, + noQuit: prefs.get(`extensions.${id}.sdk.test.no-quit`, false) + }); + this.loader = loader; + + const module = Module("package.json", `${baseURI}package.json`); + const require = Require(loader, module); + const main = command === "test" ? "sdk/test/runner" : null; + const prefsURI = `${baseURI}defaults/preferences/prefs.js`; + + const { startup } = require("sdk/addon/runner"); + startup(reason, {loader, main, prefsURI}); + }.bind(this)).catch(error => { + console.error(`Failed to start ${id} addon`, error); + throw error; + }); + }, + shutdown(addon, code) { + const { loader, domain } = this; + + this.unmount(); + this.unload(REASON[code]); + }, + unload(reason) { + const {loader} = this; + if (loader) { + this.loader = null; + unload(loader, reason); + setTimeout(() => { + for (let uri of Object.keys(loader.sandboxes)) { + Cu.nukeSandbox(loader.sandboxes[uri]); + delete loader.sandboxes[uri]; + delete loader.modules[uri]; + } + }, 1000); + } + } +}; +exports.Bootstrap = Bootstrap; diff --git a/addon-sdk/source/lib/sdk/clipboard.js b/addon-sdk/source/lib/sdk/clipboard.js index f3e71b065617..66f4608be951 100644 --- a/addon-sdk/source/lib/sdk/clipboard.js +++ b/addon-sdk/source/lib/sdk/clipboard.js @@ -9,7 +9,8 @@ module.metadata = { "engines": { // TODO Fennec Support 789757 "Firefox": "*", - "SeaMonkey": "*" + "SeaMonkey": "*", + "Thunderbird": "*" } }; @@ -124,26 +125,24 @@ exports.set = function(aData, aDataType) { switch (flavor) { case "text/html": // add text/html flavor - let (str = Cc["@mozilla.org/supports-string;1"]. - createInstance(Ci.nsISupportsString)) - { - str.data = options.data; - xferable.addDataFlavor(flavor); - xferable.setTransferData(flavor, str, str.data.length * 2); - } + let str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + + str.data = options.data; + xferable.addDataFlavor(flavor); + xferable.setTransferData(flavor, str, str.data.length * 2); // add a text/unicode flavor (html converted to plain text) - let (str = Cc["@mozilla.org/supports-string;1"]. - createInstance(Ci.nsISupportsString), - converter = Cc["@mozilla.org/feed-textconstruct;1"]. - createInstance(Ci.nsIFeedTextConstruct)) - { - converter.type = "html"; - converter.text = options.data; - str.data = converter.plainText(); - xferable.addDataFlavor("text/unicode"); - xferable.setTransferData("text/unicode", str, str.data.length * 2); - } + str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + let converter = Cc["@mozilla.org/feed-textconstruct;1"]. + createInstance(Ci.nsIFeedTextConstruct); + + converter.type = "html"; + converter.text = options.data; + str.data = converter.plainText(); + xferable.addDataFlavor("text/unicode"); + xferable.setTransferData("text/unicode", str, str.data.length * 2); break; // Set images to the clipboard is not straightforward, to have an idea how diff --git a/addon-sdk/source/lib/sdk/content/worker-parent.js b/addon-sdk/source/lib/sdk/content/worker-parent.js deleted file mode 100644 index a129fc43dc71..000000000000 --- a/addon-sdk/source/lib/sdk/content/worker-parent.js +++ /dev/null @@ -1,184 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -module.metadata = { - "stability": "unstable" -}; - -const { emit } = require('../event/core'); -const { omit } = require('../util/object'); -const { Class } = require('../core/heritage'); -const { method } = require('../lang/functional'); -const { getInnerId } = require('../window/utils'); -const { EventTarget } = require('../event/target'); -const { when, ensure } = require('../system/unload'); -const { getTabForWindow } = require('../tabs/helpers'); -const { getTabForContentWindow, getBrowserForTab } = require('../tabs/utils'); -const { isPrivate } = require('../private-browsing/utils'); -const { getFrameElement } = require('../window/utils'); -const { attach, detach, destroy } = require('./utils'); -const { on: observe } = require('../system/events'); -const { uuid } = require('../util/uuid'); -const { Ci, Cc } = require('chrome'); - -const ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]. - getService(Ci.nsIMessageBroadcaster); - -// null-out cycles in .modules to make @loader/options JSONable -const ADDON = omit(require('@loader/options'), ['modules', 'globals']); - -const workers = new WeakMap(); -let modelFor = (worker) => workers.get(worker); - -const ERR_DESTROYED = "Couldn't find the worker to receive this message. " + - "The script may not be initialized yet, or may already have been unloaded."; - -const ERR_FROZEN = "The page is currently hidden and can no longer be used " + - "until it is visible again."; - -// a handle for communication between content script and addon code -const Worker = Class({ - implements: [EventTarget], - initialize(options = {}) { - - let model = { - inited: false, - earlyEvents: [], // fired before worker was inited - frozen: true, // document is in BFcache, let it go - options, - }; - workers.set(this, model); - - ensure(this, 'destroy'); - this.on('detach', this.detach); - EventTarget.prototype.initialize.call(this, options); - - this.receive = this.receive.bind(this); - - model.observe = ({ subject }) => { - let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data; - if (model.window && getInnerId(model.window) === id) - this.detach(); - } - - observe('inner-window-destroyed', model.observe); - - this.port = EventTarget(); - this.port.emit = this.send.bind(this, 'event'); - this.postMessage = this.send.bind(this, 'message'); - - if ('window' in options) - attach(this, options.window); - }, - // messages - receive({ data: { id, args }}) { - let model = modelFor(this); - if (id !== model.id || !model.childWorker) - return; - if (args[0] === 'event') - emit(this.port, ...args.slice(1)) - else - emit(this, ...args); - }, - send(...args) { - let model = modelFor(this); - if (!model.inited) { - model.earlyEvents.push(args); - return; - } - if (!model.childWorker && args[0] !== 'detach') - throw new Error(ERR_DESTROYED); - if (model.frozen && args[0] !== 'detach') - throw new Error(ERR_FROZEN); - try { - model.manager.sendAsyncMessage('sdk/worker/message', { id: model.id, args }); - } catch (e) { - // - } - }, - // properties - get url() { - let { window } = modelFor(this); - return window && window.document.location.href; - }, - get contentURL() { - let { window } = modelFor(this); - return window && window.document.URL; - }, - get tab() { - let { window } = modelFor(this); - return window && getTabForWindow(window); - }, - toString: () => '[object Worker]', - // methods - attach: method(attach), - detach: method(detach), - destroy: method(destroy), -}) -exports.Worker = Worker; - -attach.define(Worker, function(worker, window) { - let model = modelFor(worker); - - model.window = window; - model.options.window = getInnerId(window); - model.id = model.options.id = String(uuid()); - - let tab = getTabForContentWindow(window); - if (tab) { - model.manager = getBrowserForTab(tab).messageManager; - } else { - model.manager = getFrameElement(window.top).frameLoader.messageManager; - } - - model.manager.addMessageListener('sdk/worker/event', worker.receive); - model.manager.addMessageListener('sdk/worker/attach', attach); - - model.manager.sendAsyncMessage('sdk/worker/create', { - options: model.options, - addon: ADDON - }); - - function attach({ data }) { - if (data.id !== model.id) - return; - model.manager.removeMessageListener('sdk/worker/attach', attach); - model.childWorker = true; - - worker.on('pageshow', () => model.frozen = false); - worker.on('pagehide', () => model.frozen = true); - - model.inited = true; - model.frozen = false; - - model.earlyEvents.forEach(args => worker.send(...args)); - emit(worker, 'attach', window); - } -}) - -// unload and release the child worker, release window reference -detach.define(Worker, function(worker, reason) { - let model = modelFor(worker); - worker.send('detach', reason); - if (!model.childWorker) - return; - - model.childWorker = null; - model.earlyEvents = []; - model.window = null; - emit(worker, 'detach'); - model.manager.removeMessageListener('sdk/worker/event', this.receive); -}) - -isPrivate.define(Worker, ({ tab }) => isPrivate(tab)); - -// unlod worker, release references -destroy.define(Worker, function(worker, reason) { - detach(worker, reason); - modelFor(worker).inited = true; -}) - -// unload Loaders used for creating WorkerChild instances in each process -when(() => ppmm.broadcastAsyncMessage('sdk/loader/unload', { data: ADDON })); diff --git a/addon-sdk/source/lib/sdk/content/worker.js b/addon-sdk/source/lib/sdk/content/worker.js index 100fc9fccfd5..a129fc43dc71 100644 --- a/addon-sdk/source/lib/sdk/content/worker.js +++ b/addon-sdk/source/lib/sdk/content/worker.js @@ -7,280 +7,178 @@ module.metadata = { "stability": "unstable" }; +const { emit } = require('../event/core'); +const { omit } = require('../util/object'); const { Class } = require('../core/heritage'); -const { EventTarget } = require('../event/target'); -const { on, off, emit, setListeners } = require('../event/core'); -const { - attach, detach, destroy -} = require('./utils'); const { method } = require('../lang/functional'); -const { Ci, Cu, Cc } = require('chrome'); -const unload = require('../system/unload'); -const events = require('../system/events'); -const { getInnerId } = require("../window/utils"); -const { WorkerSandbox } = require('./sandbox'); +const { getInnerId } = require('../window/utils'); +const { EventTarget } = require('../event/target'); +const { when, ensure } = require('../system/unload'); const { getTabForWindow } = require('../tabs/helpers'); +const { getTabForContentWindow, getBrowserForTab } = require('../tabs/utils'); const { isPrivate } = require('../private-browsing/utils'); +const { getFrameElement } = require('../window/utils'); +const { attach, detach, destroy } = require('./utils'); +const { on: observe } = require('../system/events'); +const { uuid } = require('../util/uuid'); +const { Ci, Cc } = require('chrome'); + +const ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]. + getService(Ci.nsIMessageBroadcaster); + +// null-out cycles in .modules to make @loader/options JSONable +const ADDON = omit(require('@loader/options'), ['modules', 'globals']); -// A weak map of workers to hold private attributes that -// should not be exposed const workers = new WeakMap(); - let modelFor = (worker) => workers.get(worker); -const ERR_DESTROYED = - "Couldn't find the worker to receive this message. " + +const ERR_DESTROYED = "Couldn't find the worker to receive this message. " + "The script may not be initialized yet, or may already have been unloaded."; const ERR_FROZEN = "The page is currently hidden and can no longer be used " + "until it is visible again."; -/** - * Message-passing facility for communication between code running - * in the content and add-on process. - * @see https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/content_worker - */ +// a handle for communication between content script and addon code const Worker = Class({ implements: [EventTarget], - initialize: function WorkerConstructor (options) { - // Save model in weak map to not expose properties - let model = createModel(); + initialize(options = {}) { + + let model = { + inited: false, + earlyEvents: [], // fired before worker was inited + frozen: true, // document is in BFcache, let it go + options, + }; workers.set(this, model); - options = options || {}; + ensure(this, 'destroy'); + this.on('detach', this.detach); + EventTarget.prototype.initialize.call(this, options); - if ('contentScriptFile' in options) - this.contentScriptFile = options.contentScriptFile; - if ('contentScriptOptions' in options) - this.contentScriptOptions = options.contentScriptOptions; - if ('contentScript' in options) - this.contentScript = options.contentScript; - if ('injectInDocument' in options) - this.injectInDocument = !!options.injectInDocument; + this.receive = this.receive.bind(this); - setListeners(this, options); + model.observe = ({ subject }) => { + let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (model.window && getInnerId(model.window) === id) + this.detach(); + } - unload.ensure(this, "destroy"); + observe('inner-window-destroyed', model.observe); - // Ensure that worker.port is initialized for contentWorker to be able - // to send events during worker initialization. - this.port = createPort(this); - - model.documentUnload = documentUnload.bind(this); - model.pageShow = pageShow.bind(this); - model.pageHide = pageHide.bind(this); + this.port = EventTarget(); + this.port.emit = this.send.bind(this, 'event'); + this.postMessage = this.send.bind(this, 'message'); if ('window' in options) attach(this, options.window); }, - - /** - * Sends a message to the worker's global scope. Method takes single - * argument, which represents data to be sent to the worker. The data may - * be any primitive type value or `JSON`. Call of this method asynchronously - * emits `message` event with data value in the global scope of this - * symbiont. - * - * `message` event listeners can be set either by calling - * `self.on` with a first argument string `"message"` or by - * implementing `onMessage` function in the global scope of this worker. - * @param {Number|String|JSON} data - */ - postMessage: function (...data) { + // messages + receive({ data: { id, args }}) { + let model = modelFor(this); + if (id !== model.id || !model.childWorker) + return; + if (args[0] === 'event') + emit(this.port, ...args.slice(1)) + else + emit(this, ...args); + }, + send(...args) { let model = modelFor(this); - let args = ['message'].concat(data); if (!model.inited) { model.earlyEvents.push(args); return; } - processMessage.apply(null, [this].concat(args)); + if (!model.childWorker && args[0] !== 'detach') + throw new Error(ERR_DESTROYED); + if (model.frozen && args[0] !== 'detach') + throw new Error(ERR_FROZEN); + try { + model.manager.sendAsyncMessage('sdk/worker/message', { id: model.id, args }); + } catch (e) { + // + } }, - - get url () { - let model = modelFor(this); - // model.window will be null after detach - return model.window ? model.window.document.location.href : null; + // properties + get url() { + let { window } = modelFor(this); + return window && window.document.location.href; }, - - get contentURL () { - let model = modelFor(this); - return model.window ? model.window.document.URL : null; + get contentURL() { + let { window } = modelFor(this); + return window && window.document.URL; }, - - get tab () { - let model = modelFor(this); - // model.window will be null after detach - if (model.window) - return getTabForWindow(model.window); - return null; + get tab() { + let { window } = modelFor(this); + return window && getTabForWindow(window); }, - - // Implemented to provide some of the previous features of exposing sandbox - // so that Worker can be extended - getSandbox: function () { - return modelFor(this).contentWorker; - }, - - toString: function () { return '[object Worker]'; }, + toString: () => '[object Worker]', + // methods attach: method(attach), detach: method(detach), - destroy: method(destroy) -}); + destroy: method(destroy), +}) exports.Worker = Worker; -attach.define(Worker, function (worker, window) { +attach.define(Worker, function(worker, window) { let model = modelFor(worker); + model.window = window; - // Track document unload to destroy this worker. - // We can't watch for unload event on page's window object as it - // prevents bfcache from working: - // https://developer.mozilla.org/En/Working_with_BFCache - model.windowID = getInnerId(model.window); - events.on("inner-window-destroyed", model.documentUnload); + model.options.window = getInnerId(window); + model.id = model.options.id = String(uuid()); - // will set model.contentWorker pointing to the private API: - model.contentWorker = WorkerSandbox(worker, model.window); + let tab = getTabForContentWindow(window); + if (tab) { + model.manager = getBrowserForTab(tab).messageManager; + } else { + model.manager = getFrameElement(window.top).frameLoader.messageManager; + } - // Listen to pagehide event in order to freeze the content script - // while the document is frozen in bfcache: - model.window.addEventListener("pageshow", model.pageShow, true); - model.window.addEventListener("pagehide", model.pageHide, true); + model.manager.addMessageListener('sdk/worker/event', worker.receive); + model.manager.addMessageListener('sdk/worker/attach', attach); - // Mainly enable worker.port.emit to send event to the content worker - model.inited = true; - model.frozen = false; + model.manager.sendAsyncMessage('sdk/worker/create', { + options: model.options, + addon: ADDON + }); - // Fire off `attach` event - emit(worker, 'attach', window); + function attach({ data }) { + if (data.id !== model.id) + return; + model.manager.removeMessageListener('sdk/worker/attach', attach); + model.childWorker = true; - // Process all events and messages that were fired before the - // worker was initialized. - model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args))); -}); + worker.on('pageshow', () => model.frozen = false); + worker.on('pagehide', () => model.frozen = true); -/** - * Remove all internal references to the attached document - * Tells _port to unload itself and removes all the references from itself. - */ -detach.define(Worker, function (worker, reason) { + model.inited = true; + model.frozen = false; + + model.earlyEvents.forEach(args => worker.send(...args)); + emit(worker, 'attach', window); + } +}) + +// unload and release the child worker, release window reference +detach.define(Worker, function(worker, reason) { let model = modelFor(worker); + worker.send('detach', reason); + if (!model.childWorker) + return; - // maybe unloaded before content side is created - if (model.contentWorker) { - model.contentWorker.destroy(reason); - } - - model.contentWorker = null; - if (model.window) { - model.window.removeEventListener("pageshow", model.pageShow, true); - model.window.removeEventListener("pagehide", model.pageHide, true); - } + model.childWorker = null; + model.earlyEvents = []; model.window = null; - // This method may be called multiple times, - // avoid dispatching `detach` event more than once - if (model.windowID) { - model.windowID = null; - events.off("inner-window-destroyed", model.documentUnload); - model.earlyEvents.length = 0; - emit(worker, 'detach'); - } - model.inited = false; -}); + emit(worker, 'detach'); + model.manager.removeMessageListener('sdk/worker/event', this.receive); +}) isPrivate.define(Worker, ({ tab }) => isPrivate(tab)); -/** - * Tells content worker to unload itself and - * removes all the references from itself. - */ -destroy.define(Worker, function (worker, reason) { +// unlod worker, release references +destroy.define(Worker, function(worker, reason) { detach(worker, reason); modelFor(worker).inited = true; - // Specifying no type or listener removes all listeners - // from target - off(worker); - off(worker.port); -}); +}) -/** - * Events fired by workers - */ -function documentUnload ({ subject, data }) { - let model = modelFor(this); - let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; - if (innerWinID != model.windowID) return false; - detach(this); - return true; -} - -function pageShow () { - let model = modelFor(this); - model.contentWorker.emitSync('pageshow'); - emit(this, 'pageshow'); - model.frozen = false; -} - -function pageHide () { - let model = modelFor(this); - model.contentWorker.emitSync('pagehide'); - emit(this, 'pagehide'); - model.frozen = true; -} - -/** - * Fired from postMessage and emitEventToContent, or from the earlyMessage - * queue when fired before the content is loaded. Sends arguments to - * contentWorker if able - */ - -function processMessage (worker, ...args) { - let model = modelFor(worker) || {}; - if (!model.contentWorker) - throw new Error(ERR_DESTROYED); - if (model.frozen) - throw new Error(ERR_FROZEN); - model.contentWorker.emit.apply(null, args); -} - -function createModel () { - return { - // List of messages fired before worker is initialized - earlyEvents: [], - // Is worker connected to the content worker sandbox ? - inited: false, - // Is worker being frozen? i.e related document is frozen in bfcache. - // Content script should not be reachable if frozen. - frozen: true, - /** - * Reference to the content side of the worker. - * @type {WorkerGlobalScope} - */ - contentWorker: null, - /** - * Reference to the window that is accessible from - * the content scripts. - * @type {Object} - */ - window: null - }; -} - -function createPort (worker) { - let port = EventTarget(); - port.emit = emitEventToContent.bind(null, worker); - return port; -} - -/** - * Emit a custom event to the content script, - * i.e. emit this event on `self.port` - */ -function emitEventToContent (worker, ...eventArgs) { - let model = modelFor(worker); - let args = ['event'].concat(eventArgs); - if (!model.inited) { - model.earlyEvents.push(args); - return; - } - processMessage.apply(null, [worker].concat(args)); -} +// unload Loaders used for creating WorkerChild instances in each process +when(() => ppmm.broadcastAsyncMessage('sdk/loader/unload', { data: ADDON })); diff --git a/addon-sdk/source/lib/sdk/context-menu.js b/addon-sdk/source/lib/sdk/context-menu.js index 004b9798dd10..7e6275df31a8 100644 --- a/addon-sdk/source/lib/sdk/context-menu.js +++ b/addon-sdk/source/lib/sdk/context-menu.js @@ -21,7 +21,6 @@ const { WindowTracker, browserWindowIterator } = require("./deprecated/window-ut const { isBrowser, getInnerId } = require("./window/utils"); const { Ci, Cc, Cu } = require("chrome"); const { MatchPattern } = require("./util/match-pattern"); -const { Worker } = require("./content/worker"); const { EventTarget } = require("./event/target"); const { emit } = require('./event/core'); const { when } = require('./system/unload'); diff --git a/addon-sdk/source/lib/sdk/context-menu/context.js b/addon-sdk/source/lib/sdk/context-menu/context.js new file mode 100644 index 000000000000..fc5aea500256 --- /dev/null +++ b/addon-sdk/source/lib/sdk/context-menu/context.js @@ -0,0 +1,147 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { Class } = require("../core/heritage"); +const { extend } = require("../util/object"); +const { MatchPattern } = require("../util/match-pattern"); +const readers = require("./readers"); + +// Context class is required to implement a single `isCurrent(target)` method +// that must return boolean value indicating weather given target matches a +// context or not. Most context implementations below will have an associated +// reader that way context implementation can setup a reader to extract necessary +// information to make decision if target is matching a context. +const Context = Class({ + isRequired: false, + isCurrent(target) { + throw Error("Context class must implement isCurrent(target) method"); + }, + get required() { + Object.defineProperty(this, "required", { + value: Object.assign(Object.create(Object.getPrototypeOf(this)), + this, + {isRequired: true}) + }); + return this.required; + } +}); +Context.required = function(...params) { + return Object.assign(new this(...params), {isRequired: true}); +}; +exports.Context = Context; + + +// Next few context implementations use an associated reader to extract info +// from the context target and story it to a private symbol associtaed with +// a context implementation. That way name collisions are avoided while required +// information is still carried along. +const isPage = Symbol("context/page?") +const PageContext = Class({ + extends: Context, + read: {[isPage]: new readers.isPage()}, + isCurrent: target => target[isPage] +}); +exports.Page = PageContext; + +const isFrame = Symbol("context/frame?"); +const FrameContext = Class({ + extends: Context, + read: {[isFrame]: new readers.isFrame()}, + isCurrent: target => target[isFrame] +}); +exports.Frame = FrameContext; + +const selection = Symbol("context/selection") +const SelectionContext = Class({ + read: {[selection]: new readers.Selection()}, + isCurrent: target => !!target[selection] +}); +exports.Selection = SelectionContext; + +const link = Symbol("context/link"); +const LinkContext = Class({ + extends: Context, + read: {[link]: new readers.LinkURL()}, + isCurrent: target => !!target[link] +}); +exports.Link = LinkContext; + +const isEditable = Symbol("context/editable?") +const EditableContext = Class({ + extends: Context, + read: {[isEditable]: new readers.isEditable()}, + isCurrent: target => target[isEditable] +}); +exports.Editable = EditableContext; + + +const mediaType = Symbol("context/mediaType") + +const ImageContext = Class({ + extends: Context, + read: {[mediaType]: new readers.MediaType()}, + isCurrent: target => target[mediaType] === "image" +}); +exports.Image = ImageContext; + + +const VideoContext = Class({ + extends: Context, + read: {[mediaType]: new readers.MediaType()}, + isCurrent: target => target[mediaType] === "video" +}); +exports.Video = VideoContext; + + +const AudioContext = Class({ + extends: Context, + read: {[mediaType]: new readers.MediaType()}, + isCurrent: target => target[mediaType] === "audio" +}); +exports.Audio = AudioContext; + +const isSelectorMatch = Symbol("context/selector/mathches?") +const SelectorContext = Class({ + extends: Context, + initialize(selector) { + this.selector = selector; + // Each instance of selector context will need to store read + // data into different field, so that case with multilpe selector + // contexts won't cause a conflicts. + this[isSelectorMatch] = Symbol(selector); + this.read = {[this[isSelectorMatch]]: new readers.SelectorMatch(selector)}; + }, + isCurrent(target) { + return target[this[isSelectorMatch]]; + } +}); +exports.Selector = SelectorContext; + +const url = Symbol("context/url"); +const URLContext = Class({ + extends: Context, + initialize(pattern) { + this.pattern = new MatchPattern(pattern); + }, + read: {[url]: new readers.PageURL()}, + isCurrent(target) { + return this.pattern.test(target[url]); + } +}); +exports.URL = URLContext; + +var PredicateContext = Class({ + extends: Context, + initialize(isMatch) { + if (typeof(isMatch) !== "function") { + throw TypeError("Predicate context mus be passed a function"); + } + + this.isMatch = isMatch + }, + isCurrent(target) { + return this.isMatch(target); + } +}); +exports.Predicate = PredicateContext; diff --git a/addon-sdk/source/lib/sdk/context-menu/core.js b/addon-sdk/source/lib/sdk/context-menu/core.js new file mode 100644 index 000000000000..c64cddfe8695 --- /dev/null +++ b/addon-sdk/source/lib/sdk/context-menu/core.js @@ -0,0 +1,384 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const Contexts = require("./context"); +const Readers = require("./readers"); +const Component = require("../ui/component"); +const { Class } = require("../core/heritage"); +const { map, filter, object, reduce, keys, symbols, + pairs, values, each, some, isEvery, count } = require("../util/sequence"); +const { loadModule } = require("framescript/manager"); +const { Cu, Cc, Ci } = require("chrome"); +const prefs = require("sdk/preferences/service"); + +const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager); +const preferencesService = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(null); + + +const readTable = Symbol("context-menu/read-table"); +const nameTable = Symbol("context-menu/name-table"); +const onContext = Symbol("context-menu/on-context"); +const isMatching = Symbol("context-menu/matching-handler?"); + +exports.onContext = onContext; +exports.readTable = readTable; +exports.nameTable = nameTable; + + +const propagateOnContext = (item, data) => + each(child => child[onContext](data), item.state.children); + +const isContextMatch = item => !item[isMatching] || item[isMatching](); + +// For whatever reason addWeakMessageListener does not seems to work as our +// instance seems to dropped even though it's alive. This is simple workaround +// to avoid dead object excetptions. +const WeakMessageListener = function(receiver, handler="receiveMessage") { + this.receiver = receiver + this.handler = handler +}; +WeakMessageListener.prototype = { + constructor: WeakMessageListener, + receiveMessage(message) { + if (Cu.isDeadWrapper(this.receiver)) { + message.target.messageManager.removeMessageListener(message.name, this); + } + else { + this.receiver[this.handler](message); + } + } +}; + +const OVERFLOW_THRESH = "extensions.addon-sdk.context-menu.overflowThreshold"; +const onMessage = Symbol("context-menu/message-listener"); +const onPreferceChange = Symbol("context-menu/preference-change"); +const ContextMenuExtension = Class({ + extends: Component, + initialize: Component, + setup() { + const messageListener = new WeakMessageListener(this, onMessage); + loadModule(globalMessageManager, "framescript/context-menu", true, "onContentFrame"); + globalMessageManager.addMessageListener("sdk/context-menu/read", messageListener); + globalMessageManager.addMessageListener("sdk/context-menu/readers?", messageListener); + + preferencesService.addObserver(OVERFLOW_THRESH, this, false); + }, + observe(_, __, name) { + if (name === OVERFLOW_THRESH) { + const overflowThreshold = prefs.get(OVERFLOW_THRESH, 10); + this[Component.patch]({overflowThreshold}); + } + }, + [onMessage]({name, data, target}) { + if (name === "sdk/context-menu/read") + this[onContext]({target, data}); + if (name === "sdk/context-menu/readers?") + target.messageManager.sendAsyncMessage("sdk/context-menu/readers", + JSON.parse(JSON.stringify(this.state.readers))); + }, + [Component.initial](options={}, children) { + const element = options.element || null; + const target = options.target || null; + const readers = Object.create(null); + const users = Object.create(null); + const registry = new WeakSet(); + const overflowThreshold = prefs.get(OVERFLOW_THRESH, 10); + + return { target, children: [], readers, users, element, + registry, overflowThreshold }; + }, + [Component.isUpdated](before, after) { + // Update only if target changed, since there is no point in re-rendering + // when children are. Also new items added won't be in sync with a latest + // context target so we should really just render before drawing context + // menu. + return before.target !== after.target; + }, + [Component.render]({element, children, overflowThreshold}) { + if (!element) return null; + + const items = children.filter(isContextMatch); + const body = items.length === 0 ? items : + items.length < overflowThreshold ? [new Separator(), + ...items] : + [{tagName: "menu", + className: "sdk-context-menu-overflow-menu", + label: "Add-ons", + accesskey: "A", + children: [{tagName: "menupopup", + children: items}]}]; + return { + element: element, + tagName: "menugroup", + style: "-moz-box-orient: vertical;", + className: "sdk-context-menu-extension", + children: body + } + }, + // Adds / remove child to it's own list. + add(item) { + this[Component.patch]({children: this.state.children.concat(item)}); + }, + remove(item) { + this[Component.patch]({ + children: this.state.children.filter(x => x !== item) + }); + }, + register(item) { + const { users, registry } = this.state; + if (registry.has(item)) return; + registry.add(item); + + // Each (ContextHandler) item has a readTable that is a + // map of keys to readers extracting them from the content. + // During the registraction we update intrnal record of unique + // readers and users per reader. Most context will have a reader + // shared across all instances there for map of users per reader + // is stored separately from the reader so that removing reader + // will occur only when no users remain. + const table = item[readTable]; + // Context readers store data in private symbols so we need to + // collect both table keys and private symbols. + const names = [...keys(table), ...symbols(table)]; + const readers = map(name => table[name], names); + // Create delta for registered readers that will be merged into + // internal readers table. + const added = filter(x => !users[x.id], readers); + const delta = object(...map(x => [x.id, x], added)); + + const update = reduce((update, reader) => { + const n = update[reader.id] || 0; + update[reader.id] = n + 1; + return update; + }, Object.assign({}, users), readers); + + // Patch current state with a changes that registered item caused. + this[Component.patch]({users: update, + readers: Object.assign(this.state.readers, delta)}); + + if (count(added)) { + globalMessageManager.broadcastAsyncMessage("sdk/context-menu/readers", + JSON.parse(JSON.stringify(delta))); + } + }, + unregister(item) { + const { users, registry } = this.state; + if (!registry.has(item)) return; + registry.delete(item); + + const table = item[readTable]; + const names = [...keys(table), ...symbols(table)]; + const readers = map(name => table[name], names); + const update = reduce((update, reader) => { + update[reader.id] = update[reader.id] - 1; + return update; + }, Object.assign({}, users), readers); + const removed = filter(id => !update[id], keys(update)); + const delta = object(...map(x => [x, null], removed)); + + this[Component.patch]({users: update, + readers: Object.assign(this.state.readers, delta)}); + + if (count(removed)) { + globalMessageManager.broadcastAsyncMessage("sdk/context-menu/readers", + JSON.parse(JSON.stringify(delta))); + } + }, + + [onContext]({data, target}) { + propagateOnContext(this, data); + const document = target.ownerDocument; + const element = document.getElementById("contentAreaContextMenu"); + + this[Component.patch]({target: data, element: element}); + } +});this, +exports.ContextMenuExtension = ContextMenuExtension; + +// Takes an item options and +const makeReadTable = ({context, read}) => { + // Result of this function is a tuple of all readers & + // name, reader id pairs. + + // Filter down to contexts that have a reader associated. + const contexts = filter(context => context.read, context); + // Merge all contexts read maps to a single hash, note that there should be + // no name collisions as context implementations expect to use private + // symbols for storing it's read data. + return Object.assign({}, ...map(({read}) => read, contexts), read); +} + +const readTarget = (nameTable, data) => + object(...map(([name, id]) => [name, data[id]], nameTable)) + +const ContextHandler = Class({ + extends: Component, + initialize: Component, + get context() { + return this.state.options.context; + }, + get read() { + return this.state.options.read; + }, + [Component.initial](options) { + return { + table: makeReadTable(options), + requiredContext: filter(context => context.isRequired, options.context), + optionalContext: filter(context => !context.isRequired, options.context) + } + }, + [isMatching]() { + const {target, requiredContext, optionalContext} = this.state; + return isEvery(context => context.isCurrent(target), requiredContext) && + (count(optionalContext) === 0 || + some(context => context.isCurrent(target), optionalContext)); + }, + setup() { + const table = makeReadTable(this.state.options); + this[readTable] = table; + this[nameTable] = [...map(symbol => [symbol, table[symbol].id], symbols(table)), + ...map(name => [name, table[name].id], keys(table))]; + + + contextMenu.register(this); + + each(child => contextMenu.remove(child), this.state.children); + contextMenu.add(this); + }, + dispose() { + contextMenu.remove(this); + + each(child => contextMenu.unregister(child), this.state.children); + contextMenu.unregister(this); + }, + // Internal `Symbol("onContext")` method is invoked when "contextmenu" event + // occurs in content process. Context handles with children delegate to each + // child and patch it's internal state to reflect new contextmenu target. + [onContext](data) { + propagateOnContext(this, data); + this[Component.patch]({target: readTarget(this[nameTable], data)}); + } +}); +const isContextHandler = item => item instanceof ContextHandler; + +exports.ContextHandler = ContextHandler; + +const Menu = Class({ + extends: ContextHandler, + [isMatching]() { + return ContextHandler.prototype[isMatching].call(this) && + this.state.children.filter(isContextHandler) + .some(isContextMatch); + }, + [Component.render]({children, options}) { + const items = children.filter(isContextMatch); + return {tagName: "menu", + className: "sdk-context-menu menu-iconic", + label: options.label, + accesskey: options.accesskey, + image: options.icon, + children: [{tagName: "menupopup", + children: items}]}; + } +}); +exports.Menu = Menu; + +const onCommand = Symbol("context-menu/item/onCommand"); +const Item = Class({ + extends: ContextHandler, + get onClick() { + return this.state.options.onClick; + }, + [Component.render]({options}) { + const {label, icon, accesskey} = options; + return {tagName: "menuitem", + className: "sdk-context-menu-item menuitem-iconic", + label, + accesskey, + image: icon, + oncommand: this}; + }, + handleEvent(event) { + if (this.onClick) + this.onClick(this.state.target); + } +}); +exports.Item = Item; + +var Separator = Class({ + extends: Component, + initialize: Component, + [Component.render]() { + return {tagName: "menuseparator", + className: "sdk-context-menu-separator"} + }, + [onContext]() { + + } +}); +exports.Separator = Separator; + +exports.Contexts = Contexts; +exports.Readers = Readers; + +const createElement = (vnode, {document}) => { + const node = vnode.namespace ? + document.createElementNS(vnode.namespace, vnode.tagName) : + document.createElement(vnode.tagName); + + node.setAttribute("data-component-path", vnode[Component.path]); + + each(([key, value]) => { + if (key === "tagName") { + return; + } + if (key === "children") { + return; + } + + if (key.startsWith("on")) { + node.addEventListener(key.substr(2), value) + return; + } + + if (typeof(value) !== "object" && + typeof(value) !== "function" && + value !== void(0) && + value !== null) + { + if (key === "className") { + node[key] = value; + } + else { + node.setAttribute(key, value); + } + return; + } + }, pairs(vnode)); + + each(child => node.appendChild(createElement(child, {document})), vnode.children); + return node; +}; + +const htmlWriter = tree => { + if (tree !== null) { + const root = tree.element; + const node = createElement(tree, {document: root.ownerDocument}); + const before = root.querySelector("[data-component-path='/']"); + if (before) { + root.replaceChild(node, before); + } else { + root.appendChild(node); + } + } +}; + + +const contextMenu = ContextMenuExtension(); +exports.contextMenu = contextMenu; +Component.mount(contextMenu, htmlWriter); diff --git a/addon-sdk/source/lib/sdk/context-menu/readers.js b/addon-sdk/source/lib/sdk/context-menu/readers.js new file mode 100644 index 000000000000..5078f8f29d21 --- /dev/null +++ b/addon-sdk/source/lib/sdk/context-menu/readers.js @@ -0,0 +1,112 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +const { Class } = require("../core/heritage"); +const { extend } = require("../util/object"); +const { memoize, method, identity } = require("../lang/functional"); + +const serializeCategory = ({type}) => ({ category: `reader/${type}()` }); + +const Reader = Class({ + initialize() { + this.id = `reader/${this.type}()` + }, + toJSON() { + return serializeCategory(this); + } +}); + + +const MediaTypeReader = Class({ extends: Reader, type: "MediaType" }); +exports.MediaType = MediaTypeReader; + +const LinkURLReader = Class({ extends: Reader, type: "LinkURL" }); +exports.LinkURL = LinkURLReader; + +const SelectionReader = Class({ extends: Reader, type: "Selection" }); +exports.Selection = SelectionReader; + +const isPageReader = Class({ extends: Reader, type: "isPage" }); +exports.isPage = isPageReader; + +const isFrameReader = Class({ extends: Reader, type: "isFrame" }); +exports.isFrame = isFrameReader; + +const isEditable = Class({ extends: Reader, type: "isEditable"}); +exports.isEditable = isEditable; + + + +const ParameterizedReader = Class({ + extends: Reader, + readParameter: function(value) { + return value; + }, + toJSON: function() { + var json = serializeCategory(this); + json[this.parameter] = this[this.parameter]; + return json; + }, + initialize(...params) { + if (params.length) { + this[this.parameter] = this.readParameter(...params); + } + this.id = `reader/${this.type}(${JSON.stringify(this[this.parameter])})`; + } +}); +exports.ParameterizedReader = ParameterizedReader; + + +const QueryReader = Class({ + extends: ParameterizedReader, + type: "Query", + parameter: "path" +}); +exports.Query = QueryReader; + + +const AttributeReader = Class({ + extends: ParameterizedReader, + type: "Attribute", + parameter: "name" +}); +exports.Attribute = AttributeReader; + +const SrcURLReader = Class({ + extends: AttributeReader, + name: "src", +}); +exports.SrcURL = SrcURLReader; + +const PageURLReader = Class({ + extends: QueryReader, + path: "ownerDocument.URL", +}); +exports.PageURL = PageURLReader; + +const SelectorMatchReader = Class({ + extends: ParameterizedReader, + type: "SelectorMatch", + parameter: "selector" +}); +exports.SelectorMatch = SelectorMatchReader; + +const extractors = new WeakMap(); +extractors.id = 0; + + +var Extractor = Class({ + extends: ParameterizedReader, + type: "Extractor", + parameter: "source", + initialize: function(f) { + this[this.parameter] = String(f); + if (!extractors.has(f)) { + extractors.id = extractors.id + 1; + extractors.set(f, extractors.id); + } + + this.id = `reader/${this.type}.for(${extractors.get(f)})` + } +}); +exports.Extractor = Extractor; diff --git a/addon-sdk/source/lib/sdk/context-menu@2.js b/addon-sdk/source/lib/sdk/context-menu@2.js new file mode 100644 index 000000000000..45ad804e9bef --- /dev/null +++ b/addon-sdk/source/lib/sdk/context-menu@2.js @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const shared = require("toolkit/require"); +const { Item, Separator, Menu, Contexts, Readers } = shared.require("sdk/context-menu/core"); +const { setupDisposable, disposeDisposable, Disposable } = require("sdk/core/disposable") +const { Class } = require("sdk/core/heritage") + +const makeDisposable = Type => Class({ + extends: Type, + implements: [Disposable], + initialize: Type.prototype.initialize, + setup(...params) { + Type.prototype.setup.call(this, ...params); + setupDisposable(this); + }, + dispose(...params) { + disposeDisposable(this); + Type.prototype.dispose.call(this, ...params); + } +}); + +exports.Separator = Separator; +exports.Contexts = Contexts; +exports.Readers = Readers; + +// Subclass Item & Menu shared classes so their items +// will be unloaded when add-on is unloaded. +exports.Item = makeDisposable(Item); +exports.Menu = makeDisposable(Menu); diff --git a/addon-sdk/source/lib/sdk/core/disposable.js b/addon-sdk/source/lib/sdk/core/disposable.js index a9f992b6f795..c9615f772299 100644 --- a/addon-sdk/source/lib/sdk/core/disposable.js +++ b/addon-sdk/source/lib/sdk/core/disposable.js @@ -52,11 +52,13 @@ setup.define(Object, (object, ...args) => object.setup(...args)); const setupDisposable = disposable => { subscribe(disposable, addonUnloadTopic, isWeak(disposable)); }; +exports.setupDisposable = setupDisposable; // Tears down disposable instance. const disposeDisposable = disposable => { unsubscribe(disposable, addonUnloadTopic); }; +exports.disposeDisposable = disposeDisposable; // Base type that takes care of disposing it's instances on add-on unload. // Also makes sure to remove unload listener if it's already being disposed. @@ -129,4 +131,3 @@ uninstall.define(Disposable, dispose); // increase shutdown time. Although specefic components may choose // to implement shutdown handler that does something better. shutdown.define(Disposable, disposable => {}); - diff --git a/addon-sdk/source/lib/sdk/deprecated/cortex.js b/addon-sdk/source/lib/sdk/deprecated/cortex.js index f06c9c10a25e..2f49cda3a79e 100644 --- a/addon-sdk/source/lib/sdk/deprecated/cortex.js +++ b/addon-sdk/source/lib/sdk/deprecated/cortex.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; module.metadata = { diff --git a/addon-sdk/source/lib/sdk/deprecated/events.js b/addon-sdk/source/lib/sdk/deprecated/events.js index b020f2d174e6..1f4509dd89cc 100644 --- a/addon-sdk/source/lib/sdk/deprecated/events.js +++ b/addon-sdk/source/lib/sdk/deprecated/events.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; module.metadata = { diff --git a/addon-sdk/source/lib/sdk/deprecated/events/assembler.js b/addon-sdk/source/lib/sdk/deprecated/events/assembler.js index 7491aebff52e..9fda00528c7b 100644 --- a/addon-sdk/source/lib/sdk/deprecated/events/assembler.js +++ b/addon-sdk/source/lib/sdk/deprecated/events/assembler.js @@ -4,7 +4,7 @@ "use strict"; -const { Trait } = require("../light-traits"); +const { Class } = require("../../core/heritage"); const { removeListener, on } = require("../../dom/events"); /** @@ -15,7 +15,7 @@ const { removeListener, on } = require("../../dom/events"); * `supportedEventsTypes` and function for handling all those events as * `handleEvent` property. */ -exports.DOMEventAssembler = Trait({ +exports.DOMEventAssembler = Class({ /** * Function that is supposed to handle all the supported events (that are * present in the `supportedEventsTypes`) from all the observed @@ -23,12 +23,16 @@ exports.DOMEventAssembler = Trait({ * @param {Event} event * Event being dispatched. */ - handleEvent: Trait.required, + handleEvent() { + throw new TypeError("Instance of DOMEventAssembler must implement `handleEvent` method"); + }, /** * Array of supported event names. * @type {String[]} */ - supportedEventsTypes: Trait.required, + get supportedEventsTypes() { + throw new TypeError("Instance of DOMEventAssembler must implement `handleEvent` field"); + }, /** * Adds `eventTarget` to the list of observed `eventTarget`s. Listeners for * supported events will be registered on the given `eventTarget`. diff --git a/addon-sdk/source/lib/sdk/deprecated/sync-worker.js b/addon-sdk/source/lib/sdk/deprecated/sync-worker.js new file mode 100644 index 000000000000..fb2b25e3cb4e --- /dev/null +++ b/addon-sdk/source/lib/sdk/deprecated/sync-worker.js @@ -0,0 +1,297 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * + * `deprecated/sync-worker` was previously `content/worker`, that was + * incompatible with e10s. we are in the process of switching to the new + * asynchronous `Worker`, which behaves slightly differently in some edge + * cases, so we are keeping this one around for a short period. + * try to switch to the new one as soon as possible.. + * + */ + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { Class } = require('../core/heritage'); +const { EventTarget } = require('../event/target'); +const { on, off, emit, setListeners } = require('../event/core'); +const { + attach, detach, destroy +} = require('../content/utils'); +const { method } = require('../lang/functional'); +const { Ci, Cu, Cc } = require('chrome'); +const unload = require('../system/unload'); +const events = require('../system/events'); +const { getInnerId } = require("../window/utils"); +const { WorkerSandbox } = require('../content/sandbox'); +const { getTabForWindow } = require('../tabs/helpers'); +const { isPrivate } = require('../private-browsing/utils'); + +// A weak map of workers to hold private attributes that +// should not be exposed +const workers = new WeakMap(); + +let modelFor = (worker) => workers.get(worker); + +const ERR_DESTROYED = + "Couldn't find the worker to receive this message. " + + "The script may not be initialized yet, or may already have been unloaded."; + +const ERR_FROZEN = "The page is currently hidden and can no longer be used " + + "until it is visible again."; + +/** + * Message-passing facility for communication between code running + * in the content and add-on process. + * @see https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/content_worker + */ +const Worker = Class({ + implements: [EventTarget], + initialize: function WorkerConstructor (options) { + // Save model in weak map to not expose properties + let model = createModel(); + workers.set(this, model); + + options = options || {}; + + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScriptOptions' in options) + this.contentScriptOptions = options.contentScriptOptions; + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('injectInDocument' in options) + this.injectInDocument = !!options.injectInDocument; + + setListeners(this, options); + + unload.ensure(this, "destroy"); + + // Ensure that worker.port is initialized for contentWorker to be able + // to send events during worker initialization. + this.port = createPort(this); + + model.documentUnload = documentUnload.bind(this); + model.pageShow = pageShow.bind(this); + model.pageHide = pageHide.bind(this); + + if ('window' in options) + attach(this, options.window); + }, + + /** + * Sends a message to the worker's global scope. Method takes single + * argument, which represents data to be sent to the worker. The data may + * be any primitive type value or `JSON`. Call of this method asynchronously + * emits `message` event with data value in the global scope of this + * symbiont. + * + * `message` event listeners can be set either by calling + * `self.on` with a first argument string `"message"` or by + * implementing `onMessage` function in the global scope of this worker. + * @param {Number|String|JSON} data + */ + postMessage: function (...data) { + let model = modelFor(this); + let args = ['message'].concat(data); + if (!model.inited) { + model.earlyEvents.push(args); + return; + } + processMessage.apply(null, [this].concat(args)); + }, + + get url () { + let model = modelFor(this); + // model.window will be null after detach + return model.window ? model.window.document.location.href : null; + }, + + get contentURL () { + let model = modelFor(this); + return model.window ? model.window.document.URL : null; + }, + + get tab () { + let model = modelFor(this); + // model.window will be null after detach + if (model.window) + return getTabForWindow(model.window); + return null; + }, + + // Implemented to provide some of the previous features of exposing sandbox + // so that Worker can be extended + getSandbox: function () { + return modelFor(this).contentWorker; + }, + + toString: function () { return '[object Worker]'; }, + attach: method(attach), + detach: method(detach), + destroy: method(destroy) +}); +exports.Worker = Worker; + +attach.define(Worker, function (worker, window) { + let model = modelFor(worker); + model.window = window; + // Track document unload to destroy this worker. + // We can't watch for unload event on page's window object as it + // prevents bfcache from working: + // https://developer.mozilla.org/En/Working_with_BFCache + model.windowID = getInnerId(model.window); + events.on("inner-window-destroyed", model.documentUnload); + + // will set model.contentWorker pointing to the private API: + model.contentWorker = WorkerSandbox(worker, model.window); + + // Listen to pagehide event in order to freeze the content script + // while the document is frozen in bfcache: + model.window.addEventListener("pageshow", model.pageShow, true); + model.window.addEventListener("pagehide", model.pageHide, true); + + // Mainly enable worker.port.emit to send event to the content worker + model.inited = true; + model.frozen = false; + + // Fire off `attach` event + emit(worker, 'attach', window); + + // Process all events and messages that were fired before the + // worker was initialized. + model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args))); +}); + +/** + * Remove all internal references to the attached document + * Tells _port to unload itself and removes all the references from itself. + */ +detach.define(Worker, function (worker, reason) { + let model = modelFor(worker); + + // maybe unloaded before content side is created + if (model.contentWorker) { + model.contentWorker.destroy(reason); + } + + model.contentWorker = null; + if (model.window) { + model.window.removeEventListener("pageshow", model.pageShow, true); + model.window.removeEventListener("pagehide", model.pageHide, true); + } + model.window = null; + // This method may be called multiple times, + // avoid dispatching `detach` event more than once + if (model.windowID) { + model.windowID = null; + events.off("inner-window-destroyed", model.documentUnload); + model.earlyEvents.length = 0; + emit(worker, 'detach'); + } + model.inited = false; +}); + +isPrivate.define(Worker, ({ tab }) => isPrivate(tab)); + +/** + * Tells content worker to unload itself and + * removes all the references from itself. + */ +destroy.define(Worker, function (worker, reason) { + detach(worker, reason); + modelFor(worker).inited = true; + // Specifying no type or listener removes all listeners + // from target + off(worker); + off(worker.port); +}); + +/** + * Events fired by workers + */ +function documentUnload ({ subject, data }) { + let model = modelFor(this); + let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (innerWinID != model.windowID) return false; + detach(this); + return true; +} + +function pageShow () { + let model = modelFor(this); + model.contentWorker.emitSync('pageshow'); + emit(this, 'pageshow'); + model.frozen = false; +} + +function pageHide () { + let model = modelFor(this); + model.contentWorker.emitSync('pagehide'); + emit(this, 'pagehide'); + model.frozen = true; +} + +/** + * Fired from postMessage and emitEventToContent, or from the earlyMessage + * queue when fired before the content is loaded. Sends arguments to + * contentWorker if able + */ + +function processMessage (worker, ...args) { + let model = modelFor(worker) || {}; + if (!model.contentWorker) + throw new Error(ERR_DESTROYED); + if (model.frozen) + throw new Error(ERR_FROZEN); + model.contentWorker.emit.apply(null, args); +} + +function createModel () { + return { + // List of messages fired before worker is initialized + earlyEvents: [], + // Is worker connected to the content worker sandbox ? + inited: false, + // Is worker being frozen? i.e related document is frozen in bfcache. + // Content script should not be reachable if frozen. + frozen: true, + /** + * Reference to the content side of the worker. + * @type {WorkerGlobalScope} + */ + contentWorker: null, + /** + * Reference to the window that is accessible from + * the content scripts. + * @type {Object} + */ + window: null + }; +} + +function createPort (worker) { + let port = EventTarget(); + port.emit = emitEventToContent.bind(null, worker); + return port; +} + +/** + * Emit a custom event to the content script, + * i.e. emit this event on `self.port` + */ +function emitEventToContent (worker, ...eventArgs) { + let model = modelFor(worker); + let args = ['event'].concat(eventArgs); + if (!model.inited) { + model.earlyEvents.push(args); + return; + } + processMessage.apply(null, [worker].concat(args)); +} diff --git a/addon-sdk/source/lib/sdk/deprecated/unit-test.js b/addon-sdk/source/lib/sdk/deprecated/unit-test.js index 245924087931..d6d1c048ab01 100644 --- a/addon-sdk/source/lib/sdk/deprecated/unit-test.js +++ b/addon-sdk/source/lib/sdk/deprecated/unit-test.js @@ -10,7 +10,7 @@ module.metadata = { const memory = require("./memory"); const timer = require("../timers"); const cfxArgs = require("../test/options"); -const { getTabs, closeTab, getURI } = require("../tabs/utils"); +const { getTabs, closeTab, getURI, getTabId, getSelectedTab } = require("../tabs/utils"); const { windows, isBrowser, getMostRecentBrowserWindow } = require("../window/utils"); const { defer, all, Debugging: PromiseDebugging, resolve } = require("../core/promise"); const { getInnerId } = require("../window/utils"); @@ -35,10 +35,16 @@ const findAndRunTests = function findAndRunTests(options) { exports.findAndRunTests = findAndRunTests; let runnerWindows = new WeakMap(); +let runnerTabs = new WeakMap(); const TestRunner = function TestRunner(options) { options = options || {}; - runnerWindows.set(this, getInnerId(getMostRecentBrowserWindow())); + + // remember the id's for the open window and tab + let window = getMostRecentBrowserWindow(); + runnerWindows.set(this, getInnerId(window)); + runnerTabs.set(this, getTabId(getSelectedTab(window))); + this.fs = options.fs; this.console = options.console || console; memory.track(this); @@ -318,15 +324,26 @@ TestRunner.prototype = { return all(winPromises).then(() => { let browserWins = wins.filter(isBrowser); let tabs = browserWins.reduce((tabs, window) => tabs.concat(getTabs(window)), []); - - if (wins.length != 1 || getInnerId(wins[0]) !== runnerWindows.get(this)) - this.fail("Should not be any unexpected windows open"); - + let newTabID = getTabId(getSelectedTab(wins[0])); + let oldTabID = runnerTabs.get(this); let hasMoreTabsOpen = browserWins.length && tabs.length != 1; - if (hasMoreTabsOpen) - this.fail("Should not be any unexpected tabs open"); + let failure = false; - if (hasMoreTabsOpen || wins.length != 1) { + if (wins.length != 1 || getInnerId(wins[0]) !== runnerWindows.get(this)) { + failure = true; + this.fail("Should not be any unexpected windows open"); + } + else if (hasMoreTabsOpen) { + failure = true; + this.fail("Should not be any unexpected tabs open"); + } + else if (oldTabID != newTabID) { + failure = true; + runnerTabs.set(this, newTabID); + this.fail("Should not be any new tabs left open, old id: " + oldTabID + " new id: " + newTabID); + } + + if (failure) { console.log("Windows open:"); for (let win of wins) { if (isBrowser(win)) { @@ -356,7 +373,7 @@ TestRunner.prototype = { timer.setTimeout(_ => onDone(this)); } }). - catch(e => console.exception(e)); + catch(console.exception); }, // Set of assertion functions to wait for an assertion to become true diff --git a/addon-sdk/source/lib/sdk/io/stream.js b/addon-sdk/source/lib/sdk/io/stream.js index 92bfc21262dd..906fb0e564e8 100644 --- a/addon-sdk/source/lib/sdk/io/stream.js +++ b/addon-sdk/source/lib/sdk/io/stream.js @@ -192,8 +192,10 @@ const InputStream = Class({ }, resume: function resume() { this.paused = false; - nsIInputStreamPump(this).resume(); - emit(this, "resume"); + if (nsIInputStreamPump(this).isPending()) { + nsIInputStreamPump(this).resume(); + emit(this, "resume"); + } }, close: function close() { this.readable = false; diff --git a/addon-sdk/source/lib/sdk/keyboard/observer.js b/addon-sdk/source/lib/sdk/keyboard/observer.js index d85bef8932d0..b3d672660dcf 100644 --- a/addon-sdk/source/lib/sdk/keyboard/observer.js +++ b/addon-sdk/source/lib/sdk/keyboard/observer.js @@ -8,8 +8,9 @@ module.metadata = { "stability": "unstable" }; -const { Trait } = require("../deprecated/light-traits"); -const { EventEmitterTrait: EventEmitter } = require("../deprecated/events"); +const { Class } = require("../core/heritage"); +const { EventTarget } = require("../event/target"); +const { emit } = require("../event/core"); const { DOMEventAssembler } = require("../deprecated/events/assembler"); const { browserWindowIterator } = require('../deprecated/window-utils'); const { isBrowser } = require('../window/utils'); @@ -17,12 +18,26 @@ const { observer: windowObserver } = require("../windows/observer"); // Event emitter objects used to register listeners and emit events on them // when they occur. -const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ - /** - * Method is implemented by `EventEmitter` and is used just for emitting - * events on registered listeners. - */ - _emit: Trait.required, +const Observer = Class({ + implements: [DOMEventAssembler, EventTarget], + initialize() { + // Adding each opened window to a list of observed windows. + windowObserver.on("open", window => { + if (isBrowser(window)) + this.observe(window); + }); + + // Removing each closed window form the list of observed windows. + windowObserver.on("close", window => { + if (isBrowser(window)) + this.ignore(window); + }); + + // Making observer aware of already opened windows. + for (let window of browserWindowIterator()) { + this.observe(window); + } + }, /** * Events that are supported and emitted by the module. */ @@ -34,24 +49,9 @@ const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ * @param {Event} event * Keyboard event being emitted. */ - handleEvent: function handleEvent(event) { - this._emit(event.type, event, event.target.ownerDocument.defaultView); + handleEvent(event) { + emit(this, event.type, event, event.target.ownerDocument.defaultView); } }); -// Adding each opened window to a list of observed windows. -windowObserver.on("open", function onOpen(window) { - if (isBrowser(window)) - observer.observe(window); -}); -// Removing each closed window form the list of observed windows. -windowObserver.on("close", function onClose(window) { - if (isBrowser(window)) - observer.ignore(window); -}); - -// Making observer aware of already opened windows. -for (let window of browserWindowIterator()) - observer.observe(window); - -exports.observer = observer; +exports.observer = new Observer(); diff --git a/addon-sdk/source/lib/sdk/l10n/locale.js b/addon-sdk/source/lib/sdk/l10n/locale.js index 831a037fd3f1..0d489742c6e5 100644 --- a/addon-sdk/source/lib/sdk/l10n/locale.js +++ b/addon-sdk/source/lib/sdk/l10n/locale.js @@ -50,7 +50,8 @@ function getPreferedLocales(caseSensitve) { addLocale(browserUiLocale); // Third priority is the list of locales used for web content - let contentLocales = prefs.get(PREF_ACCEPT_LANGUAGES, ""); + let contentLocales = prefs.getLocalized(PREF_ACCEPT_LANGUAGES, "") || + prefs.get(PREF_ACCEPT_LANGUAGES, ""); if (contentLocales) { // This list is a string of locales seperated by commas. // There is spaces after commas, so strip each item diff --git a/addon-sdk/source/lib/sdk/lang/type.js b/addon-sdk/source/lib/sdk/lang/type.js index 8d2feea4e194..bbf6cba306f3 100644 --- a/addon-sdk/source/lib/sdk/lang/type.js +++ b/addon-sdk/source/lib/sdk/lang/type.js @@ -107,6 +107,18 @@ function isObject(value) { } exports.isObject = isObject; +/** + * Detect whether a value is a generator. + * + * @param aValue + * The value to identify. + * @return A boolean indicating whether the value is a generator. + */ +function isGenerator(aValue) { + return !!(aValue && aValue.isGenerator && aValue.isGenerator()); +} +exports.isGenerator = isGenerator; + /** * Returns true if `value` is an Array. * @examples diff --git a/addon-sdk/source/lib/sdk/loader/cuddlefish.js b/addon-sdk/source/lib/sdk/loader/cuddlefish.js index f6b8b45cb15a..a44ab93e96ab 100644 --- a/addon-sdk/source/lib/sdk/loader/cuddlefish.js +++ b/addon-sdk/source/lib/sdk/loader/cuddlefish.js @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -'use strict'; +"use strict"; module.metadata = { "stability": "unstable" @@ -14,7 +14,6 @@ module.metadata = { require('chrome') // Otherwise CFX will complain about Components require('toolkit/loader') // Otherwise CFX will stip out loader.js require('sdk/addon/runner') // Otherwise CFX will stip out addon/runner.js -require('sdk/system/xul-app') // Otherwise CFX will stip out sdk/system/xul-app */ const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components; @@ -23,65 +22,17 @@ const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components; const loaderURI = module.uri.replace("sdk/loader/cuddlefish.js", "toolkit/loader.js"); const xulappURI = module.uri.replace("loader/cuddlefish.js", - "system/xul-app.js"); + "system/xul-app.jsm"); // We need to keep a reference to the sandbox in order to unload it in // bootstrap.js const loaderSandbox = loadSandbox(loaderURI); const loaderModule = loaderSandbox.exports; -const xulappSandbox = loadSandbox(xulappURI); -const xulappModule = xulappSandbox.exports; +const { incompatibility } = Cu.import(xulappURI, {}).XulApp; const { override, load } = loaderModule; -/** - * Ensure the current application satisfied the requirements specified in the - * module given. If not, an exception related to the incompatibility is - * returned; `null` otherwise. - * - * @param {Object} module - * The module to check - * @returns {Error} - */ -function incompatibility(module) { - let { metadata, id } = module; - - // if metadata or engines are not specified we assume compatibility is not - // an issue. - if (!metadata || !("engines" in metadata)) - return null; - - let { engines } = metadata; - - if (engines === null || typeof(engines) !== "object") - return new Error("Malformed engines' property in metadata"); - - let applications = Object.keys(engines); - - let versionRange; - applications.forEach(function(name) { - if (xulappModule.is(name)) { - versionRange = engines[name]; - // Continue iteration. We want to ensure the module doesn't - // contain a typo in the applications' name or some unknown - // application - `is` function throws an exception in that case. - } - }); - - if (typeof(versionRange) === "string") { - if (xulappModule.satisfiesVersion(versionRange)) - return null; - - return new Error("Unsupported Application version: The module " + id + - " currently supports only version " + versionRange + " of " + - xulappModule.name + "."); - } - - return new Error("Unsupported Application: The module " + id + - " currently supports only " + applications.join(", ") + ".") -} - function CuddlefishLoader(options) { let { manifest } = options; @@ -90,8 +41,7 @@ function CuddlefishLoader(options) { // cache to avoid subsequent loads via `require`. modules: override({ 'toolkit/loader': loaderModule, - 'sdk/loader/cuddlefish': exports, - 'sdk/system/xul-app': xulappModule + 'sdk/loader/cuddlefish': exports }, options.modules), resolve: function resolve(id, requirer) { let entry = requirer && requirer in manifest && manifest[requirer]; diff --git a/addon-sdk/source/lib/sdk/page-mod.js b/addon-sdk/source/lib/sdk/page-mod.js index e4073c3f661d..cc3ee3b49242 100644 --- a/addon-sdk/source/lib/sdk/page-mod.js +++ b/addon-sdk/source/lib/sdk/page-mod.js @@ -14,7 +14,7 @@ const { getAttachEventType, WorkerHost } = require('./content/utils'); const { Class } = require('./core/heritage'); const { Disposable } = require('./core/disposable'); const { WeakReference } = require('./core/reference'); -const { Worker } = require('./content/worker-parent'); +const { Worker } = require('./content/worker'); const { EventTarget } = require('./event/target'); const { on, emit, once, setListeners } = require('./event/core'); const { on: domOn, removeListener: domOff } = require('./dom/events'); @@ -189,6 +189,10 @@ function applyOnExistingDocuments (mod) { getTabs().forEach(tab => { // Fake a newly created document let window = getTabContentWindow(tab); + // on startup with e10s, contentWindow might not exist yet, + // in which case we will get notified by "document-element-inserted". + if (!window || !window.frames) + return; let uri = getTabURI(tab); if (has(mod.attachTo, "top") && modMatchesURI(mod, uri)) onContent(mod, window); @@ -216,7 +220,7 @@ function createWorker (mod, window) { // page-mod's "attach" event needs a worker if (event === 'attach') emit(mod, event, worker) - else + else emit(mod, event, ...args); }) once(worker, 'detach', () => worker.destroy()); diff --git a/addon-sdk/source/lib/sdk/page-mod/match-pattern.js b/addon-sdk/source/lib/sdk/page-mod/match-pattern.js index 37b7bfb8a391..10a837a15f0f 100644 --- a/addon-sdk/source/lib/sdk/page-mod/match-pattern.js +++ b/addon-sdk/source/lib/sdk/page-mod/match-pattern.js @@ -1,3 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + "use strict"; + let { deprecateUsage } = require("../util/deprecate"); deprecateUsage("Module 'sdk/page-mod/match-pattern' is deprecated use 'sdk/util/match-pattern' instead"); diff --git a/addon-sdk/source/lib/sdk/page-worker.js b/addon-sdk/source/lib/sdk/page-worker.js index 6a9d053483ee..99d0692b1318 100644 --- a/addon-sdk/source/lib/sdk/page-worker.js +++ b/addon-sdk/source/lib/sdk/page-worker.js @@ -28,9 +28,9 @@ const { Rules } = require('./util/rules'); const { merge } = require('./util/object'); const { data } = require('./self'); -const views = WeakMap(); -const workers = WeakMap(); -const pages = WeakMap(); +const views = new WeakMap(); +const workers = new WeakMap(); +const pages = new WeakMap(); const readyEventNames = [ 'DOMContentLoaded', @@ -136,7 +136,7 @@ const Page = Class({ // page-worker doesn't have a model like other APIs, so to be consitent // with the behavior "what you set is what you get", we need to store - // the original `contentURL` given. + // the original `contentURL` given. // Even if XUL elements doesn't support `dataset`, properties, to // indicate that is a custom attribute the syntax "data-*" is used. view.setAttribute('data-src', contentURL); diff --git a/addon-sdk/source/lib/sdk/panel.js b/addon-sdk/source/lib/sdk/panel.js index 881f5828c621..c09ad1a435bf 100644 --- a/addon-sdk/source/lib/sdk/panel.js +++ b/addon-sdk/source/lib/sdk/panel.js @@ -15,12 +15,10 @@ module.metadata = { const { Ci } = require("chrome"); const { setTimeout } = require('./timers'); -const { isPrivateBrowsingSupported } = require('./self'); -const { isWindowPBSupported } = require('./private-browsing/utils'); const { Class } = require("./core/heritage"); const { merge } = require("./util/object"); const { WorkerHost } = require("./content/utils"); -const { Worker } = require("./content/worker"); +const { Worker } = require("./deprecated/sync-worker"); const { Disposable } = require("./core/disposable"); const { WeakReference } = require('./core/reference'); const { contract: loaderContract } = require("./content/loader"); @@ -155,7 +153,7 @@ const Panel = Class({ // Load panel content. domPanel.setURL(view, model.contentURL); - + // Allow context menu domPanel.allowContextMenu(view, model.contextMenu); @@ -195,7 +193,7 @@ const Panel = Class({ /* Public API: Panel.position */ get position() modelFor(this).position, - + /* Public API: Panel.contextMenu */ get contextMenu() modelFor(this).contextMenu, set contextMenu(allow) { @@ -203,7 +201,7 @@ const Panel = Class({ model.contextMenu = panelContract({ contextMenu: allow }).contextMenu; domPanel.allowContextMenu(viewFor(this), model.contextMenu); }, - + get contentURL() modelFor(this).contentURL, set contentURL(value) { let model = modelFor(this); diff --git a/addon-sdk/source/lib/sdk/panel/utils.js b/addon-sdk/source/lib/sdk/panel/utils.js index e86a3a577290..6cd765262442 100644 --- a/addon-sdk/source/lib/sdk/panel/utils.js +++ b/addon-sdk/source/lib/sdk/panel/utils.js @@ -213,7 +213,7 @@ function show(panel, options, anchor) { // Prevent the panel from getting focus when showing up // if focus is set to false panel.setAttribute("noautofocus", !options.focus); - + let window = anchor && getOwnerBrowserWindow(anchor); let { document } = window ? window : getMostRecentBrowserWindow(); attach(panel, document); @@ -286,8 +286,7 @@ function make(document) { events.emit(type, { subject: panel }); } - function onContentChange({subject, type}) { - let document = subject; + function onContentChange({subject: document, type}) { if (document === getContentDocument(panel) && document.defaultView) events.emit(type, { subject: panel }); } @@ -411,9 +410,9 @@ function setURL(panel, url) { exports.setURL = setURL; function allowContextMenu(panel, allow) { - if(allow) { + if (allow) { panel.setAttribute("context", "contentAreaContextMenu"); - } + } else { panel.removeAttribute("context"); } diff --git a/addon-sdk/source/lib/sdk/panel/window.js b/addon-sdk/source/lib/sdk/panel/window.js deleted file mode 100644 index 22a829e4c00a..000000000000 --- a/addon-sdk/source/lib/sdk/panel/window.js +++ /dev/null @@ -1,64 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -'use strict'; - -// The panel module currently supports only Firefox. -// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps -module.metadata = { - 'stability': 'unstable', - 'engines': { - 'Firefox': '*' - } -}; - -const { getMostRecentBrowserWindow, windows: getWindows } = require('../window/utils'); -const { ignoreWindow } = require('../private-browsing/utils'); -const { isPrivateBrowsingSupported } = require('../self'); - -function getWindow(anchor) { - let window; - let windows = getWindows("navigator:browser", { - includePrivate: isPrivateBrowsingSupported - }); - - if (anchor) { - let anchorWindow = anchor.ownerDocument.defaultView.top; - let anchorDocument = anchorWindow.document; - - // loop thru supported windows - for (let enumWindow of windows) { - // Check if the anchor is in this browser window. - if (enumWindow == anchorWindow) { - window = anchorWindow; - break; - } - - // Check if the anchor is in a browser tab in this browser window. - try { - let browser = enumWindow.gBrowser.getBrowserForDocument(anchorDocument); - if (browser) { - window = enumWindow; - break; - } - } - catch (e) { - } - - // Look in other subdocuments (sidebar, etc.)? - } - } - - // If we didn't find the anchor's window (or we have no anchor), - // return the most recent browser window. - if (!window) - window = getMostRecentBrowserWindow(); - - // if the window is not supported, then it should be ignored - if (ignoreWindow(window)) { - return null; - } - - return window; -} -exports.getWindow = getWindow; diff --git a/addon-sdk/source/lib/sdk/places/bookmarks.js b/addon-sdk/source/lib/sdk/places/bookmarks.js index f5d200e254f1..937dd8c3ef98 100644 --- a/addon-sdk/source/lib/sdk/places/bookmarks.js +++ b/addon-sdk/source/lib/sdk/places/bookmarks.js @@ -7,7 +7,8 @@ module.metadata = { "stability": "unstable", "engines": { - "Firefox": "*" + "Firefox": "*", + "SeaMonkey": "*" } }; diff --git a/addon-sdk/source/lib/sdk/places/events.js b/addon-sdk/source/lib/sdk/places/events.js index 4703d2a65277..610ce0f79669 100644 --- a/addon-sdk/source/lib/sdk/places/events.js +++ b/addon-sdk/source/lib/sdk/places/events.js @@ -7,7 +7,8 @@ module.metadata = { 'stability': 'experimental', 'engines': { - 'Firefox': '*' + 'Firefox': '*', + "SeaMonkey": '*' } }; diff --git a/addon-sdk/source/lib/sdk/places/favicon.js b/addon-sdk/source/lib/sdk/places/favicon.js index 1d0198c2d138..05b057db1129 100644 --- a/addon-sdk/source/lib/sdk/places/favicon.js +++ b/addon-sdk/source/lib/sdk/places/favicon.js @@ -7,7 +7,8 @@ module.metadata = { "stability": "unstable", "engines": { - "Firefox": "*" + "Firefox": "*", + "SeaMonkey": "*" } }; diff --git a/addon-sdk/source/lib/sdk/places/history.js b/addon-sdk/source/lib/sdk/places/history.js index 31df8af672e1..eaf1e0f45989 100644 --- a/addon-sdk/source/lib/sdk/places/history.js +++ b/addon-sdk/source/lib/sdk/places/history.js @@ -7,7 +7,8 @@ module.metadata = { "stability": "unstable", "engines": { - "Firefox": "*" + "Firefox": "*", + "SeaMonkey": "*" } }; diff --git a/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js b/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js index 6891c71d2cc7..ee5c742b48d2 100644 --- a/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js +++ b/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js @@ -7,7 +7,8 @@ module.metadata = { "stability": "experimental", "engines": { - "Firefox": "*" + "Firefox": "*", + "SeaMonkey": "*" } }; diff --git a/addon-sdk/source/lib/sdk/places/host/host-query.js b/addon-sdk/source/lib/sdk/places/host/host-query.js index 8bbb71d7b047..12ba203afcd6 100644 --- a/addon-sdk/source/lib/sdk/places/host/host-query.js +++ b/addon-sdk/source/lib/sdk/places/host/host-query.js @@ -7,7 +7,8 @@ module.metadata = { "stability": "experimental", "engines": { - "Firefox": "*" + "Firefox": "*", + "SeaMonkey": "*" } }; diff --git a/addon-sdk/source/lib/sdk/places/host/host-tags.js b/addon-sdk/source/lib/sdk/places/host/host-tags.js index d3e22e33ddea..c3c171067a09 100644 --- a/addon-sdk/source/lib/sdk/places/host/host-tags.js +++ b/addon-sdk/source/lib/sdk/places/host/host-tags.js @@ -7,7 +7,8 @@ module.metadata = { "stability": "experimental", "engines": { - "Firefox": "*" + "Firefox": "*", + "SeaMonkey": "*" } }; diff --git a/addon-sdk/source/lib/sdk/places/utils.js b/addon-sdk/source/lib/sdk/places/utils.js index 3673c6bd4971..e016a1452532 100644 --- a/addon-sdk/source/lib/sdk/places/utils.js +++ b/addon-sdk/source/lib/sdk/places/utils.js @@ -7,7 +7,8 @@ module.metadata = { "stability": "experimental", "engines": { - "Firefox": "*" + "Firefox": "*", + "SeaMonkey": "*" } }; diff --git a/addon-sdk/source/lib/sdk/preferences/native-options.js b/addon-sdk/source/lib/sdk/preferences/native-options.js index 59d10a11b059..46e174c95d60 100644 --- a/addon-sdk/source/lib/sdk/preferences/native-options.js +++ b/addon-sdk/source/lib/sdk/preferences/native-options.js @@ -123,7 +123,8 @@ function injectOptions({ preferences, preferencesBranch, document, parent, id }) setting.setAttribute('pref', 'extensions.' + preferencesBranch + '.' + name); setting.setAttribute('type', type); setting.setAttribute('title', title); - setting.setAttribute('desc', description); + if (description) + setting.setAttribute('desc', description); if (type === 'file' || type === 'directory') { setting.setAttribute('fullpath', 'true'); diff --git a/addon-sdk/source/lib/sdk/preferences/service.js b/addon-sdk/source/lib/sdk/preferences/service.js index 4ad2df01b627..beb4ec429158 100644 --- a/addon-sdk/source/lib/sdk/preferences/service.js +++ b/addon-sdk/source/lib/sdk/preferences/service.js @@ -21,106 +21,70 @@ const prefService = Cc["@mozilla.org/preferences-service;1"]. const prefSvc = prefService.getBranch(null); const defaultBranch = prefService.getDefaultBranch(null); -function Branch(branchName) { - function getPrefKeys() { - return keys(branchName).map(function(key) { - return key.replace(branchName, ""); - }); - } +const { Preferences } = require("resource://gre/modules/Preferences.jsm"); +const prefs = new Preferences({}); - return Proxy.create({ - get: function(receiver, pref) { - return get(branchName + pref); - }, - set: function(receiver, pref, val) { - set(branchName + pref, val); - }, - delete: function(pref) { - reset(branchName + pref); - return true; - }, - has: function hasPrefKey(pref) { - return has(branchName + pref) - }, - getPropertyDescriptor: function(name) { +const branchKeys = branchName => + keys(branchName).map($ => $.replace(branchName, "")); + +const Branch = function(branchName) { + return new Proxy(Branch.prototype, { + getOwnPropertyDescriptor(target, name, receiver) { return { - value: get(branchName + name) + configurable: true, + enumerable: true, + writable: false, + value: this.get(target, name, receiver) }; }, - enumerate: getPrefKeys, - keys: getPrefKeys - }, Branch.prototype); + enumerate(target) { + return branchKeys(branchName)[Symbol.iterator](); + }, + ownKeys(target) { + return branchKeys(branchName); + }, + get(target, name, receiver) { + return get(`${branchName}${name}`); + }, + set(target, name, value, receiver) { + set(`${branchName}${name}`, value); + }, + has(target, name) { + return this.hasOwn(target, name); + }, + hasOwn(target, name) { + return has(`${branchName}${name}`); + }, + deleteProperty(target, name) { + reset(`${branchName}${name}`); + return true; + } + }); } + function get(name, defaultValue) { - switch (prefSvc.getPrefType(name)) { - case Ci.nsIPrefBranch.PREF_STRING: - return prefSvc.getComplexValue(name, Ci.nsISupportsString).data; - - case Ci.nsIPrefBranch.PREF_INT: - return prefSvc.getIntPref(name); - - case Ci.nsIPrefBranch.PREF_BOOL: - return prefSvc.getBoolPref(name); - - case Ci.nsIPrefBranch.PREF_INVALID: - return defaultValue; - - default: - // This should never happen. - throw new Error("Error getting pref " + name + - "; its value's type is " + - prefSvc.getPrefType(name) + - ", which I don't know " + - "how to handle."); - } + return prefs.get(name, defaultValue); } exports.get = get; + function set(name, value) { var prefType; if (typeof value != "undefined" && value != null) prefType = value.constructor.name; switch (prefType) { - case "String": - { - var string = Cc["@mozilla.org/supports-string;1"]. - createInstance(Ci.nsISupportsString); - string.data = value; - prefSvc.setComplexValue(name, Ci.nsISupportsString, string); - } - break; - case "Number": - // We throw if the number is outside the range or not an integer, since - // the result will not be what the consumer wanted to store. - if (value > MAX_INT || value < MIN_INT) - throw new Error("you cannot set the " + name + - " pref to the number " + value + - ", as number pref values must be in the signed " + - "32-bit integer range -(2^31) to 2^31-1. " + - "To store numbers outside that range, store " + - "them as strings."); if (value % 1 != 0) throw new Error("cannot store non-integer number: " + value); - prefSvc.setIntPref(name, value); - break; - - case "Boolean": - prefSvc.setBoolPref(name, value); - break; - - default: - throw new Error("can't set pref " + name + " to value '" + value + - "'; it isn't a string, integer, or boolean"); } + + prefs.set(name, value); } exports.set = set; -function has(name) { - return (prefSvc.getPrefType(name) != Ci.nsIPrefBranch.PREF_INVALID); -} +const has = prefs.has.bind(prefs) exports.has = has; function keys(root) { @@ -128,9 +92,7 @@ function keys(root) { } exports.keys = keys; -function isSet(name) { - return (has(name) && prefSvc.prefHasUserValue(name)); -} +const isSet = prefs.isSet.bind(prefs); exports.isSet = isSet; function reset(name) { diff --git a/addon-sdk/source/lib/sdk/preferences/utils.js b/addon-sdk/source/lib/sdk/preferences/utils.js index 1b46741816d2..1d5769c37def 100644 --- a/addon-sdk/source/lib/sdk/preferences/utils.js +++ b/addon-sdk/source/lib/sdk/preferences/utils.js @@ -8,20 +8,17 @@ module.metadata = { }; const { openTab, getBrowserForTab, getTabId } = require("sdk/tabs/utils"); -const { defer, all } = require("sdk/core/promise"); const { on, off } = require("sdk/system/events"); -const { setTimeout } = require("sdk/timers"); const { getMostRecentBrowserWindow } = require('../window/utils'); -const open = function open({ id }) { - let showing = defer(); - let loaded = defer(); - let result = { id: id }; - let tab = openTab(getMostRecentBrowserWindow(), "about:addons", { - inBackground: true - }); +// Opens about:addons in a new tab, then displays the inline +// preferences of the provided add-on +const open = ({ id }) => new Promise((resolve, reject) => { + // opening the about:addons page in a new tab + let tab = openTab(getMostRecentBrowserWindow(), "about:addons"); let browser = getBrowserForTab(tab); + // waiting for the about:addons page to load browser.addEventListener("load", function onPageLoad() { browser.removeEventListener("load", onPageLoad, true); let window = browser.contentWindow; @@ -30,21 +27,16 @@ const open = function open({ id }) { on("addon-options-displayed", function onPrefDisplayed({ subject: doc, data }) { if (data === id) { off("addon-options-displayed", onPrefDisplayed); - result.tabId = getTabId(tab); - result.document = doc; - loaded.resolve(); + resolve({ + id: id, + tabId: getTabId(tab), + "document": doc + }); } }, true); // display the add-on inline preferences page window.gViewController.commands.cmd_showItemDetails.doCommand({ id: id }, true); - let { node } = window.gViewController.viewObjects.detail; - node.addEventListener("ViewChanged", function whenViewChanges() { - node.removeEventListener("ViewChanged", whenViewChanges, false); - showing.resolve(); - }, false); }, true); - - return all([ showing.promise, loaded.promise ]).then(_ => result); -} +}); exports.open = open; diff --git a/addon-sdk/source/lib/sdk/self.js b/addon-sdk/source/lib/sdk/self.js index 1a4d88680d70..198aa1661848 100644 --- a/addon-sdk/source/lib/sdk/self.js +++ b/addon-sdk/source/lib/sdk/self.js @@ -21,7 +21,7 @@ const name = readPref("name") || options.name; const version = readPref("version") || options.version; const loadReason = readPref("load.reason") || options.loadReason; const rootURI = readPref("rootURI") || options.rootURI || ""; -const baseURI = readPref("baseURI") || options.prefixURI + name + "/"; +const baseURI = readPref("baseURI") || options.prefixURI + name + "/" const addonDataURI = baseURI + "data/"; const metadata = options.metadata || {}; const permissions = metadata.permissions || {}; @@ -30,7 +30,10 @@ const isPacked = rootURI && rootURI.indexOf("jar:") === 0; const uri = (path="") => path.contains(":") ? path : addonDataURI + path.replace(/^\.\//, ""); -let { preferencesBranch } = options; +let preferencesBranch = ("preferences-branch" in metadata) + ? metadata["preferences-branch"] + : options.preferencesBranch + if (/[^\w{@}.-]/.test(preferencesBranch)) { preferencesBranch = id; console.warn("Ignoring preferences-branch (not a valid branch name)"); diff --git a/addon-sdk/source/lib/sdk/system/child_process.js b/addon-sdk/source/lib/sdk/system/child_process.js index 1ffbf9ddf615..40093fbc26af 100644 --- a/addon-sdk/source/lib/sdk/system/child_process.js +++ b/addon-sdk/source/lib/sdk/system/child_process.js @@ -20,7 +20,7 @@ let { merge } = require('../util/object'); let { setTimeout, clearTimeout } = require('../timers'); let isWindows = platform.indexOf('win') === 0; -let processes = WeakMap(); +let processes = new WeakMap(); /** diff --git a/addon-sdk/source/lib/sdk/system/xul-app.js b/addon-sdk/source/lib/sdk/system/xul-app.js index 971f8e326785..612386f77cd0 100644 --- a/addon-sdk/source/lib/sdk/system/xul-app.js +++ b/addon-sdk/source/lib/sdk/system/xul-app.js @@ -7,7 +7,6 @@ module.metadata = { "stability": "experimental" }; -var { Cu } = require("chrome"); -var { XulApp } = Cu.import("resource://gre/modules/sdk/system/XulApp.js", {}); +const { XulApp } = require("./xul-app.jsm"); Object.keys(XulApp).forEach(k => exports[k] = XulApp[k]); diff --git a/addon-sdk/source/modules/system/XulApp.js b/addon-sdk/source/lib/sdk/system/xul-app.jsm similarity index 75% rename from addon-sdk/source/modules/system/XulApp.js rename to addon-sdk/source/lib/sdk/system/xul-app.jsm index 68148be3a80d..b118b272138b 100644 --- a/addon-sdk/source/modules/system/XulApp.js +++ b/addon-sdk/source/lib/sdk/system/xul-app.jsm @@ -3,15 +3,26 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -var EXPORTED_SYMBOLS = ["XulApp"]; +this.EXPORTED_SYMBOLS = [ "XulApp" ]; var { classes: Cc, interfaces: Ci } = Components; var exports = {}; -var XulApp = exports; +this.XulApp = exports; -var appInfo = Cc["@mozilla.org/xre/app-info;1"] +var appInfo; + +// NOTE: below is required to avoid failing xpcshell tests, +// which do not implement nsIXULAppInfo +// See Bug 1114752 https://bugzilla.mozilla.org/show_bug.cgi?id=1114752 +try { + appInfo = Cc["@mozilla.org/xre/app-info;1"] .getService(Ci.nsIXULAppInfo); +} +catch (e) { + // xpcshell test case + appInfo = {}; +} var vc = Cc["@mozilla.org/xpcom/version-comparator;1"] .getService(Ci.nsIVersionComparator); @@ -28,13 +39,10 @@ var platformVersion = exports.platformVersion = appInfo.platformVersion; // re-branded versions of a product have different names: for instance, // Firefox, Minefield, Iceweasel, and Shiretoko all have the same // GUID. -// This mapping is duplicated in `app-extensions/bootstrap.js`. They should keep -// in sync, so if you change one, change the other too! var ids = exports.ids = { Firefox: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", Mozilla: "{86c18b42-e466-45a9-ae7a-9b95ba6f5640}", - Sunbird: "{718e30fb-e89b-41dd-9da7-e25a45638b28}", SeaMonkey: "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}", Fennec: "{aa3c5121-dab2-40e2-81ca-7ea25febc110}", Thunderbird: "{3550f703-e582-4d05-9a08-453d09bdfdc6}" @@ -183,3 +191,51 @@ function satisfiesVersion(version, versionRange) { }); } exports.satisfiesVersion = satisfiesVersion; + +/** + * Ensure the current application satisfied the requirements specified in the + * module given. If not, an exception related to the incompatibility is + * returned; `null` otherwise. + * + * @param {Object} module + * The module to check + * @returns {Error} + */ +function incompatibility(module) { + let { metadata, id } = module; + + // if metadata or engines are not specified we assume compatibility is not + // an issue. + if (!metadata || !("engines" in metadata)) + return null; + + let { engines } = metadata; + + if (engines === null || typeof(engines) !== "object") + return new Error("Malformed engines' property in metadata"); + + let applications = Object.keys(engines); + + let versionRange; + applications.forEach(function(name) { + if (is(name)) { + versionRange = engines[name]; + // Continue iteration. We want to ensure the module doesn't + // contain a typo in the applications' name or some unknown + // application - `is` function throws an exception in that case. + } + }); + + if (typeof(versionRange) === "string") { + if (satisfiesVersion(versionRange)) + return null; + + return new Error("Unsupported Application version: The module " + id + + " currently supports only version " + versionRange + " of " + + name + "."); + } + + return new Error("Unsupported Application: The module " + id + + " currently supports only " + applications.join(", ") + ".") +} +exports.incompatibility = incompatibility; diff --git a/addon-sdk/source/lib/sdk/tabs/observer.js b/addon-sdk/source/lib/sdk/tabs/observer.js index 5596674ef1b9..f3f2f9caa44f 100644 --- a/addon-sdk/source/lib/sdk/tabs/observer.js +++ b/addon-sdk/source/lib/sdk/tabs/observer.js @@ -7,12 +7,13 @@ module.metadata = { "stability": "unstable" }; -const { EventEmitterTrait: EventEmitter } = require("../deprecated/events"); +const { EventTarget } = require("../event/target"); +const { emit } = require("../event/core"); const { DOMEventAssembler } = require("../deprecated/events/assembler"); -const { Trait } = require("../deprecated/light-traits"); -const { getActiveTab, getTabs, getTabContainer } = require("./utils"); +const { Class } = require("../core/heritage"); +const { getActiveTab, getTabs } = require("./utils"); const { browserWindowIterator } = require("../deprecated/window-utils"); -const { isBrowser } = require('../window/utils'); +const { isBrowser, windows, getMostRecentBrowserWindow } = require("../window/utils"); const { observer: windowObserver } = require("../windows/observer"); const EVENTS = { @@ -24,15 +25,69 @@ const EVENTS = { "TabUnpinned": "unpinned" }; +const selectedTab = Symbol("observer/state/selectedTab"); // Event emitter objects used to register listeners and emit events on them // when they occur. -const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ - /** - * Method is implemented by `EventEmitter` and is used just for emitting - * events on registered listeners. - */ - _emit: Trait.required, +const Observer = Class({ + implements: [EventTarget, DOMEventAssembler], + initialize() { + this[selectedTab] = null; + // Currently Gecko does not dispatch any event on the previously selected + // tab before / after "TabSelect" is dispatched. In order to work around this + // limitation we keep track of selected tab and emit "deactivate" event with + // that before emitting "activate" on selected tab. + this.on("select", tab => { + const selected = this[selectedTab]; + if (selected !== tab) { + if (selected) { + emit(this, 'deactivate', selected); + } + + if (tab) { + this[selectedTab] = tab; + emit(this, 'activate', this[selectedTab]); + } + } + }); + + + // We also observe opening / closing windows in order to add / remove it's + // containers to the observed list. + windowObserver.on("open", chromeWindow => { + if (isBrowser(chromeWindow)) { + this.observe(chromeWindow); + } + }); + + windowObserver.on("close", chromeWindow => { + if (isBrowser(chromeWindow)) { + // Bug 751546: Emit `deactivate` event on window close immediatly + // Otherwise we are going to face "dead object" exception on `select` event + if (getActiveTab(chromeWindow) === this[selectedTab]) { + emit(this, "deactivate", this[selectedTab]); + this[selectedTab] = null; + } + this.ignore(chromeWindow); + } + }); + + + // Currently gecko does not dispatches "TabSelect" events when different + // window gets activated. To work around this limitation we emulate "select" + // event for this case. + windowObserver.on("activate", chromeWindow => { + if (isBrowser(chromeWindow)) { + emit(this, "select", getActiveTab(chromeWindow)); + } + }); + + // We should synchronize state, since probably we already have at least one + // window open. + for (let chromeWindow of browserWindowIterator()) { + this.observe(chromeWindow); + } + }, /** * Events that are supported and emitted by the module. */ @@ -45,54 +100,8 @@ const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ * Keyboard event being emitted. */ handleEvent: function handleEvent(event) { - this._emit(EVENTS[event.type], event.target, event); + emit(this, EVENTS[event.type], event.target, event); } }); -// Currently Gecko does not dispatch any event on the previously selected -// tab before / after "TabSelect" is dispatched. In order to work around this -// limitation we keep track of selected tab and emit "deactivate" event with -// that before emitting "activate" on selected tab. -var selectedTab = null; -function onTabSelect(tab) { - if (selectedTab !== tab) { - if (selectedTab) observer._emit('deactivate', selectedTab); - if (tab) observer._emit('activate', selectedTab = tab); - } -}; -observer.on('select', onTabSelect); - -// We also observe opening / closing windows in order to add / remove it's -// containers to the observed list. -function onWindowOpen(chromeWindow) { - if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. - observer.observe(getTabContainer(chromeWindow)); -} -windowObserver.on("open", onWindowOpen); - -function onWindowClose(chromeWindow) { - if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. - // Bug 751546: Emit `deactivate` event on window close immediatly - // Otherwise we are going to face "dead object" exception on `select` event - if (getActiveTab(chromeWindow) == selectedTab) { - observer._emit("deactivate", selectedTab); - selectedTab = null; - } - observer.ignore(getTabContainer(chromeWindow)); -} -windowObserver.on("close", onWindowClose); - - -// Currently gecko does not dispatches "TabSelect" events when different -// window gets activated. To work around this limitation we emulate "select" -// event for this case. -windowObserver.on("activate", function onWindowActivate(chromeWindow) { - if (!isBrowser(chromeWindow)) return; // Ignore if it's not a browser window. - observer._emit("select", getActiveTab(chromeWindow)); -}); - -// We should synchronize state, since probably we already have at least one -// window open. -for (let window of browserWindowIterator()) onWindowOpen(window); - -exports.observer = observer; +exports.observer = new Observer(); diff --git a/addon-sdk/source/lib/sdk/tabs/worker.js b/addon-sdk/source/lib/sdk/tabs/worker.js index bf1893b4fd1f..d2ba336960c5 100644 --- a/addon-sdk/source/lib/sdk/tabs/worker.js +++ b/addon-sdk/source/lib/sdk/tabs/worker.js @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict'; -const ContentWorker = require('../content/worker-parent').Worker; +const ContentWorker = require('../content/worker').Worker; function Worker(options, window) { options.window = window; diff --git a/addon-sdk/source/lib/sdk/test.js b/addon-sdk/source/lib/sdk/test.js index 4d6ab2cf1ab0..e7e3df84084b 100644 --- a/addon-sdk/source/lib/sdk/test.js +++ b/addon-sdk/source/lib/sdk/test.js @@ -8,10 +8,10 @@ module.metadata = { }; const { Cu } = require("chrome"); -const { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); +const { Task } = require("resource://gre/modules/Task.jsm", {}); const { defer } = require("sdk/core/promise"); const BaseAssert = require("sdk/test/assert").Assert; -const { isFunction, isObject } = require("sdk/lang/type"); +const { isFunction, isObject, isGenerator } = require("sdk/lang/type"); const { extend } = require("sdk/util/object"); exports.Assert = BaseAssert; @@ -49,10 +49,10 @@ function defineTestSuite(target, suite, prefix) { // If test function is a generator use a task JS to allow yield-ing // style test runs. - if (test.isGenerator && test.isGenerator()) { + if (isGenerator(test)) { options.waitUntilDone(); Task.spawn(test.bind(null, assert)). - then(null, assert.fail). + catch(assert.fail). then(assert.end); } @@ -60,7 +60,6 @@ function defineTestSuite(target, suite, prefix) { // it means that test is async and second argument is a callback // to notify that test is finished. else if (1 < test.length) { - // Letting test runner know that test is executed async and // creating a callback function that CommonJS tests will call // once it's done. diff --git a/addon-sdk/source/lib/sdk/test/assert.js b/addon-sdk/source/lib/sdk/test/assert.js index f3802ed4162b..8478c841486f 100644 --- a/addon-sdk/source/lib/sdk/test/assert.js +++ b/addon-sdk/source/lib/sdk/test/assert.js @@ -78,9 +78,9 @@ Assert.prototype = { if ('operator' in e) { message += [ " -", - source(e.expected), + source(e.actual), e.operator, - source(e.actual) + source(e.expected) ].join(" "); } } @@ -89,6 +89,7 @@ Assert.prototype = { }, pass: function pass(message) { this._log.pass(message); + return true; }, error: function error(e) { this._log.exception(e); @@ -101,10 +102,11 @@ Assert.prototype = { message: message, operator: "==" }); + return false; } - else { - this.pass(message); - } + + this.pass(message); + return true; }, /** @@ -115,15 +117,16 @@ Assert.prototype = { equal: function equal(actual, expected, message) { if (actual == expected) { this.pass(message); + return true; } - else { - this.fail({ - actual: actual, - expected: expected, - message: message, - operator: "==" - }); - } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "==" + }); + return false; }, /** @@ -135,15 +138,16 @@ Assert.prototype = { notEqual: function notEqual(actual, expected, message) { if (actual != expected) { this.pass(message); + return true; } - else { - this.fail({ - actual: actual, - expected: expected, - message: message, - operator: "!=", - }); - } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "!=", + }); + return false; }, /** @@ -154,15 +158,16 @@ Assert.prototype = { deepEqual: function deepEqual(actual, expected, message) { if (isDeepEqual(actual, expected)) { this.pass(message); + return true; } - else { - this.fail({ - actual: actual, - expected: expected, - message: message, - operator: "deepEqual" - }); - } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "deepEqual" + }); + return false; }, /** @@ -174,15 +179,16 @@ Assert.prototype = { notDeepEqual: function notDeepEqual(actual, expected, message) { if (!isDeepEqual(actual, expected)) { this.pass(message); + return true; } - else { - this.fail({ - actual: actual, - expected: expected, - message: message, - operator: "notDeepEqual" - }); - } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "notDeepEqual" + }); + return false; }, /** @@ -194,15 +200,16 @@ Assert.prototype = { strictEqual: function strictEqual(actual, expected, message) { if (actual === expected) { this.pass(message); + return true; } - else { - this.fail({ - actual: actual, - expected: expected, - message: message, - operator: "===" - }); - } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "===" + }); + return false; }, /** @@ -214,15 +221,16 @@ Assert.prototype = { notStrictEqual: function notStrictEqual(actual, expected, message) { if (actual !== expected) { this.pass(message); + return true; } - else { - this.fail({ - actual: actual, - expected: expected, - message: message, - operator: "!==" - }) - } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "!==" + }); + return false; }, /** @@ -275,35 +283,36 @@ Assert.prototype = { if (threw && (isUndefined(Error) || // If passed `Error` is RegExp using it's test method to // assert thrown exception message. - (isRegExp(Error) && Error.test(exception.message)) || + (isRegExp(Error) && (Error.test(exception.message) || Error.test(exception.toString()))) || // If passed `Error` is a constructor function testing if // thrown exception is an instance of it. (isFunction(Error) && instanceOf(exception, Error)))) { this.pass(message); + return true; } // Otherwise we report assertion failure. - else { - let failure = { - message: message, - operator: "throws" - }; + let failure = { + message: message, + operator: "matches" + }; - if (exception) - failure.actual = exception; - - if (Error) - failure.expected = Error; - - this.fail(failure); + if (exception) { + failure.actual = exception.message || exception.toString(); } + + if (Error) { + failure.expected = Error.toString(); + } + + this.fail(failure); + return false; } }; exports.Assert = Assert; function isDeepEqual(actual, expected) { - // 7.1. All identical values are equivalent, as determined by ===. if (actual === expected) { return true; diff --git a/addon-sdk/source/lib/sdk/test/harness.js b/addon-sdk/source/lib/sdk/test/harness.js index 7302739dcc11..aefccd886aaa 100644 --- a/addon-sdk/source/lib/sdk/test/harness.js +++ b/addon-sdk/source/lib/sdk/test/harness.js @@ -58,11 +58,7 @@ var stopOnError; var findAndRunTests; // Combined information from all test runs. -var results = { - passed: 0, - failed: 0, - testRuns: [] -}; +var results; // A list of the compartments and windows loaded after startup var startLeaks; @@ -438,7 +434,8 @@ var POINTLESS_ERRORS = [ 'file: "chrome://browser/content/', 'file: "chrome://global/content/', '[JavaScript Warning: "The character encoding of a framed document was ' + - 'not declared.' + 'not declared.', + 'file: "chrome://browser/skin/' ]; var consoleListener = { @@ -590,6 +587,12 @@ var runTests = exports.runTests = function runTests(options) { print = options.print; findAndRunTests = options.findAndRunTests; + results = { + passed: 0, + failed: 0, + testRuns: [] + }; + try { consoleListener.register(); print("Running tests on " + system.name + " " + system.version + diff --git a/addon-sdk/source/lib/sdk/test/memory.js b/addon-sdk/source/lib/sdk/test/memory.js index 3ac644d3fc97..1c564331266f 100644 --- a/addon-sdk/source/lib/sdk/test/memory.js +++ b/addon-sdk/source/lib/sdk/test/memory.js @@ -1,3 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict'; const { Cu } = require("chrome"); diff --git a/addon-sdk/source/lib/sdk/test/utils.js b/addon-sdk/source/lib/sdk/test/utils.js index 67522a2df6e1..24c714c30122 100644 --- a/addon-sdk/source/lib/sdk/test/utils.js +++ b/addon-sdk/source/lib/sdk/test/utils.js @@ -12,6 +12,9 @@ const { setInterval, clearInterval } = require('../timers'); const { getTabs, closeTab } = require("../tabs/utils"); const { windows: getWindows } = require("../window/utils"); const { close: closeWindow } = require("../window/helpers"); +const { isGenerator } = require("../lang/type"); + +const { Task } = require("resource://gre/modules/Task.jsm"); function getTestNames (exports) Object.keys(exports).filter(name => /^test/.test(name)) @@ -29,16 +32,37 @@ function isHelperAsync (fn) fn.length > 2 function before (exports, beforeFn) { getTestNames(exports).map(name => { let testFn = exports[name]; - if (!isTestAsync(testFn) && !isHelperAsync(beforeFn)) { - exports[name] = function (assert) { + + // GENERATOR TESTS + if (isGenerator(testFn) && isGenerator(beforeFn)) { + exports[name] = function*(assert) { + yield Task.spawn(beforeFn.bind(null, name, assert)); + yield Task.spawn(testFn.bind(null, assert)); + } + } + else if (isGenerator(testFn) && !isHelperAsync(beforeFn)) { + exports[name] = function*(assert) { beforeFn(name, assert); + yield Task.spawn(testFn.bind(null, assert)); + } + } + else if (isGenerator(testFn) && isHelperAsync(beforeFn)) { + exports[name] = function*(assert) { + yield new Promise(resolve => beforeFn(name, assert, resolve)); + yield Task.spawn(testFn.bind(null, assert)); + } + } + // SYNC TESTS + else if (!isTestAsync(testFn) && isGenerator(beforeFn)) { + exports[name] = function*(assert) { + yield Task.spawn(beforeFn.bind(null, name, assert)); testFn(assert); }; } - else if (isTestAsync(testFn) && !isHelperAsync(beforeFn)) { - exports[name] = function (assert, done) { + else if (!isTestAsync(testFn) && !isHelperAsync(beforeFn)) { + exports[name] = function (assert) { beforeFn(name, assert); - testFn(assert, done); + testFn(assert); }; } else if (!isTestAsync(testFn) && isHelperAsync(beforeFn)) { @@ -48,7 +72,21 @@ function before (exports, beforeFn) { done(); }); }; - } else if (isTestAsync(testFn) && isHelperAsync(beforeFn)) { + } + // ASYNC TESTS + else if (isTestAsync(testFn) && isGenerator(beforeFn)) { + exports[name] = function*(assert) { + yield Task.spawn(beforeFn.bind(null, name, assert)); + yield new Promise(resolve => testFn(assert, resolve)); + }; + } + else if (isTestAsync(testFn) && !isHelperAsync(beforeFn)) { + exports[name] = function (assert, done) { + beforeFn(name, assert); + testFn(assert, done); + }; + } + else if (isTestAsync(testFn) && isHelperAsync(beforeFn)) { exports[name] = function (assert, done) { beforeFn(name, assert, () => { testFn(assert, done); @@ -69,30 +107,62 @@ exports.before = before; function after (exports, afterFn) { getTestNames(exports).map(name => { let testFn = exports[name]; - if (!isTestAsync(testFn) && !isHelperAsync(afterFn)) { + + // GENERATOR TESTS + if (isGenerator(testFn) && isGenerator(afterFn)) { + exports[name] = function*(assert) { + yield Task.spawn(testFn.bind(null, assert)); + yield Task.spawn(afterFn.bind(null, name, assert)); + } + } + else if (isGenerator(testFn) && !isHelperAsync(afterFn)) { + exports[name] = function*(assert) { + yield Task.spawn(testFn.bind(null, assert)); + afterFn(name, assert); + } + } + else if (isGenerator(testFn) && isHelperAsync(afterFn)) { + exports[name] = function*(assert) { + yield Task.spawn(testFn.bind(null, assert)); + yield new Promise(resolve => afterFn(name, assert, resolve)); + } + } + // SYNC TESTS + else if (!isTestAsync(testFn) && isGenerator(afterFn)) { + exports[name] = function*(assert) { + testFn(assert); + yield Task.spawn(afterFn.bind(null, name, assert)); + }; + } + else if (!isTestAsync(testFn) && !isHelperAsync(afterFn)) { exports[name] = function (assert) { testFn(assert); afterFn(name, assert); }; } - else if (isTestAsync(testFn) && !isHelperAsync(afterFn)) { - exports[name] = function (assert, done) { - testFn(assert, () => { - afterFn(name, assert); - done(); - }); - }; - } else if (!isTestAsync(testFn) && isHelperAsync(afterFn)) { exports[name] = function (assert, done) { testFn(assert); afterFn(name, assert, done); }; - } else if (isTestAsync(testFn) && isHelperAsync(afterFn)) { - exports[name] = function (assert, done) { - testFn(assert, () => { - afterFn(name, assert, done); - }); + } + // ASYNC TESTS + else if (isTestAsync(testFn) && isGenerator(afterFn)) { + exports[name] = function*(assert) { + yield new Promise(resolve => testFn(assert, resolve)); + yield Task.spawn(afterFn.bind(null, name, assert)); + }; + } + else if (isTestAsync(testFn) && !isHelperAsync(afterFn)) { + exports[name] = function*(assert) { + yield new Promise(resolve => testFn(assert, resolve)); + afterFn(name, assert); + }; + } + else if (isTestAsync(testFn) && isHelperAsync(afterFn)) { + exports[name] = function*(assert) { + yield new Promise(resolve => testFn(assert, resolve)); + yield new Promise(resolve => afterFn(name, assert, resolve)); }; } }); diff --git a/addon-sdk/source/lib/sdk/ui/button/view/events.js b/addon-sdk/source/lib/sdk/ui/button/view/events.js index d0a5cd9ee563..14de8d96174d 100644 --- a/addon-sdk/source/lib/sdk/ui/button/view/events.js +++ b/addon-sdk/source/lib/sdk/ui/button/view/events.js @@ -7,7 +7,9 @@ module.metadata = { 'stability': 'experimental', 'engines': { - 'Firefox': '*' + 'Firefox': '*', + 'SeaMonkey': '*', + 'Thunderbird': '*' } }; diff --git a/addon-sdk/source/lib/sdk/ui/component.js b/addon-sdk/source/lib/sdk/ui/component.js new file mode 100644 index 000000000000..d1f12c95ec97 --- /dev/null +++ b/addon-sdk/source/lib/sdk/ui/component.js @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// Internal properties not exposed to the public. +const cache = Symbol("component/cache"); +const writer = Symbol("component/writer"); +const isFirstWrite = Symbol("component/writer/first-write?"); +const currentState = Symbol("component/state/current"); +const pendingState = Symbol("component/state/pending"); +const isWriting = Symbol("component/writing?"); + +const isntNull = x => x !== null; + +const Component = function(options, children) { + this[currentState] = null; + this[pendingState] = null; + this[writer] = null; + this[cache] = null; + this[isFirstWrite] = true; + + this[Component.construct](options, children); +} +Component.Component = Component; +// Constructs component. +Component.construct = Symbol("component/construct"); +// Called with `options` and `children` and must return +// initial state back. +Component.initial = Symbol("component/initial"); + +// Function patches current `state` with a given update. +Component.patch = Symbol("component/patch"); +// Function that replaces current `state` with a passed state. +Component.reset = Symbol("component/reset"); + +// Function that must return render tree from passed state. +Component.render = Symbol("component/render"); + +// Path of the component with in the mount point. +Component.path = Symbol("component/path"); + +Component.isMounted = component => !!component[writer]; +Component.isWriting = component => !!component[isWriting]; + +// Internal method that mounts component to a writer. +// Mounts component to a writer. +Component.mount = (component, write) => { + if (Component.isMounted(component)) { + throw Error("Can not mount already mounted component"); + } + + component[writer] = write; + Component.write(component); + + if (component[Component.mounted]) { + component[Component.mounted](); + } +} + +// Unmounts component from a writer. +Component.unmount = (component) => { + if (Component.isMounted(component)) { + component[writer] = null; + if (component[Component.unmounted]) { + component[Component.unmounted](); + } + } else { + console.warn("Unmounting component that is not mounted is redundant"); + } +}; + // Method invoked once after inital write occurs. +Component.mounted = Symbol("component/mounted"); +// Internal method that unmounts component from the writer. +Component.unmounted = Symbol("component/unmounted"); +// Function that must return true if component is changed +Component.isUpdated = Symbol("component/updated?"); +Component.update = Symbol("component/update"); +Component.updated = Symbol("component/updated"); + +const writeChild = base => (child, index) => Component.write(child, base, index) +Component.write = (component, base, index) => { + if (component === null) { + return component; + } + + if (!(component instanceof Component)) { + const path = base ? `${base}${component.key || index}/` : `/`; + return Object.assign({}, component, { + [Component.path]: path, + children: component.children && component.children. + map(writeChild(path)). + filter(isntNull) + }); + } + + component[isWriting] = true; + + try { + + const current = component[currentState]; + const pending = component[pendingState] || current; + const isUpdated = component[Component.isUpdated]; + const isInitial = component[isFirstWrite]; + + if (isUpdated(current, pending) || isInitial) { + if (!isInitial && component[Component.update]) { + component[Component.update](pending, current) + } + + // Note: [Component.update] could have caused more updates so can't use + // `pending` as `component[pendingState]` may have changed. + component[currentState] = component[pendingState] || current; + component[pendingState] = null; + + const tree = component[Component.render](component[currentState]); + component[cache] = Component.write(tree, base, index); + if (component[writer]) { + component[writer].call(null, component[cache]); + } + + if (!isInitial && component[Component.updated]) { + component[Component.updated](current, pending); + } + } + + component[isFirstWrite] = false; + + return component[cache]; + } finally { + component[isWriting] = false; + } +}; + +Component.prototype = Object.freeze({ + constructor: Component, + + [Component.mounted]: null, + [Component.unmounted]: null, + [Component.update]: null, + [Component.updated]: null, + + get state() { + return this[pendingState] || this[currentState]; + }, + + + [Component.construct](settings, items) { + const initial = this[Component.initial]; + const base = initial(settings, items); + const options = Object.assign(Object.create(null), base.options, settings); + const children = base.children || items || null; + const state = Object.assign(Object.create(null), base, {options, children}); + this[currentState] = state; + + if (this.setup) { + this.setup(state); + } + }, + [Component.initial](options, children) { + return Object.create(null); + }, + [Component.patch](update) { + this[Component.reset](Object.assign({}, this.state, update)); + }, + [Component.reset](state) { + this[pendingState] = state; + if (Component.isMounted(this) && !Component.isWriting(this)) { + Component.write(this); + } + }, + + [Component.isUpdated](before, after) { + return before != after + }, + + [Component.render](state) { + throw Error("Component must implement [Component.render] member"); + } +}); + +module.exports = Component; diff --git a/addon-sdk/source/lib/sdk/ui/sidebar.js b/addon-sdk/source/lib/sdk/ui/sidebar.js index fa71c09026e3..ba611fd987a0 100644 --- a/addon-sdk/source/lib/sdk/ui/sidebar.js +++ b/addon-sdk/source/lib/sdk/ui/sidebar.js @@ -24,7 +24,7 @@ const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = requ const { ns } = require('../core/namespace'); const { remove: removeFromArray } = require('../util/array'); const { show, hide, toggle } = require('./sidebar/actions'); -const { Worker } = require('../content/worker'); +const { Worker } = require('../deprecated/sync-worker'); const { contract: sidebarContract } = require('./sidebar/contract'); const { create, dispose, updateTitle, updateURL, isSidebarShowing, showSidebar, hideSidebar } = require('./sidebar/view'); const { defer } = require('../core/promise'); diff --git a/addon-sdk/source/lib/sdk/ui/state.js b/addon-sdk/source/lib/sdk/ui/state.js index dd3e6d3282e0..8ce24f2957b6 100644 --- a/addon-sdk/source/lib/sdk/ui/state.js +++ b/addon-sdk/source/lib/sdk/ui/state.js @@ -8,7 +8,9 @@ module.metadata = { 'stability': 'experimental', 'engines': { - 'Firefox': '*' + 'Firefox': '*', + 'SeaMonkey': '*', + 'Thunderbird': '*' } }; diff --git a/addon-sdk/source/lib/sdk/ui/state/events.js b/addon-sdk/source/lib/sdk/ui/state/events.js index d0a5cd9ee563..14de8d96174d 100644 --- a/addon-sdk/source/lib/sdk/ui/state/events.js +++ b/addon-sdk/source/lib/sdk/ui/state/events.js @@ -7,7 +7,9 @@ module.metadata = { 'stability': 'experimental', 'engines': { - 'Firefox': '*' + 'Firefox': '*', + 'SeaMonkey': '*', + 'Thunderbird': '*' } }; diff --git a/addon-sdk/source/lib/sdk/uri/resource.js b/addon-sdk/source/lib/sdk/uri/resource.js new file mode 100644 index 000000000000..8a1dcbf2c126 --- /dev/null +++ b/addon-sdk/source/lib/sdk/uri/resource.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const {Cc, Ci} = require("chrome"); +const ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); +const resourceHandler = ioService.getProtocolHandler("resource"). + QueryInterface(Ci.nsIResProtocolHandler); + +const URI = (uri, base=null) => + ioService.newURI(uri, null, base && URI(base)) + +const mount = (domain, uri) => + resourceHandler.setSubstitution(domain, ioService.newURI(uri, null, null)); +exports.mount = mount; + +const unmount = (domain, uri) => + resourceHandler.setSubstitution(domain, null); +exports.unmount = unmount; + +const domain = 1; +const path = 2; +const resolve = (uri) => { + const match = /resource\:\/\/([^\/]+)\/{0,1}([\s\S]*)/.exec(uri); + const domain = match && match[1]; + const path = match && match[2]; + return !match ? null : + !resourceHandler.hasSubstitution(domain) ? null : + resourceHandler.resolveURI(URI(`/${path}`, `resource://${domain}/`)); +} +exports.resolve = resolve; diff --git a/addon-sdk/source/lib/sdk/util/bond.js b/addon-sdk/source/lib/sdk/util/bond.js new file mode 100644 index 000000000000..422f57737e17 --- /dev/null +++ b/addon-sdk/source/lib/sdk/util/bond.js @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +const makeDescriptor = (name, method) => ({ + get() { + if (!Object.hasOwnProperty.call(this, name)) { + Object.defineProperty(this, name, {value: method.bind(this)}); + return this[name]; + } else { + return method; + } + } +}); + +const Bond = function(methods) { + let descriptor = {}; + let members = [...Object.getOwnPropertyNames(methods), + ...Object.getOwnPropertySymbols(methods)]; + + for (let name of members) { + let method = methods[name]; + if (typeof(method) !== "function") { + throw new TypeError(`Property named "${name}" passed to Bond must be a function`); + } + descriptor[name] = makeDescriptor(name, method); + } + + return Object.create(Bond.prototype, descriptor); +} +exports.Bond = Bond; diff --git a/addon-sdk/source/lib/sdk/util/registry.js b/addon-sdk/source/lib/sdk/util/registry.js deleted file mode 100644 index a0122ee50588..000000000000 --- a/addon-sdk/source/lib/sdk/util/registry.js +++ /dev/null @@ -1,59 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -module.metadata = { - "stability": "unstable" -}; - -const { EventEmitter } = require('../deprecated/events'); -const unload = require('../system/unload'); - -const Registry = EventEmitter.compose({ - _registry: null, - _constructor: null, - constructor: function Registry(constructor) { - this._registry = []; - this._constructor = constructor; - this.on('error', this._onError = this._onError.bind(this)); - unload.ensure(this, "_destructor"); - }, - _destructor: function _destructor() { - let _registry = this._registry.slice(0); - for (let instance of _registry) - this._emit('remove', instance); - this._registry.splice(0); - }, - _onError: function _onError(e) { - if (!this._listeners('error').length) - console.error(e); - }, - has: function has(instance) { - let _registry = this._registry; - return ( - (0 <= _registry.indexOf(instance)) || - (instance && instance._public && 0 <= _registry.indexOf(instance._public)) - ); - }, - add: function add(instance) { - let { _constructor, _registry } = this; - if (!(instance instanceof _constructor)) - instance = new _constructor(instance); - if (0 > _registry.indexOf(instance)) { - _registry.push(instance); - this._emit('add', instance); - } - return instance; - }, - remove: function remove(instance) { - let _registry = this._registry; - let index = _registry.indexOf(instance) - if (0 <= index) { - this._emit('remove', instance); - _registry.splice(index, 1); - } - } -}); -exports.Registry = Registry; - diff --git a/addon-sdk/source/lib/sdk/util/sequence.js b/addon-sdk/source/lib/sdk/util/sequence.js index afb19985b7ef..e10563a9a36b 100644 --- a/addon-sdk/source/lib/sdk/util/sequence.js +++ b/addon-sdk/source/lib/sdk/util/sequence.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; module.metadata = { @@ -22,14 +21,15 @@ module.metadata = { // - `_` used for argument(s) or variable(s) who's values are ignored. const { complement, flip, identity } = require("../lang/functional"); -const { isArray, isArguments, isMap, isSet, +const { isArray, isArguments, isMap, isSet, isGenerator, isString, isBoolean, isNumber } = require("../lang/type"); const Sequence = function Sequence(iterator) { - if (iterator.isGenerator && iterator.isGenerator()) - this[Symbol.iterator] = iterator; - else + if (!isGenerator(iterator)) { throw TypeError("Expected generator argument"); + } + + this[Symbol.iterator] = iterator; }; exports.Sequence = Sequence; @@ -61,9 +61,6 @@ const seq = polymorphic({ }); exports.seq = seq; - - - // Function to cast seq to string. const string = (...etc) => "".concat(...etc); exports.string = string; @@ -111,6 +108,27 @@ const pairs = polymorphic({ }); exports.pairs = pairs; +const names = polymorphic({ + null: empty, + void: empty, + default: object => seq(function*() { + for (let name of Object.getOwnPropertyNames(object)) { + yield name; + } + }) +}); +exports.names = names; + +const symbols = polymorphic({ + null: empty, + void: empty, + default: object => seq(function* () { + for (let symbol of Object.getOwnPropertySymbols(object)) { + yield symbol; + } + }) +}); +exports.symbols = symbols; const keys = polymorphic({ null: empty, diff --git a/addon-sdk/source/lib/sdk/window/utils.js b/addon-sdk/source/lib/sdk/window/utils.js index ff1d5572068c..54a40e0ffbd6 100644 --- a/addon-sdk/source/lib/sdk/window/utils.js +++ b/addon-sdk/source/lib/sdk/window/utils.js @@ -25,8 +25,9 @@ const FM = Cc["@mozilla.org/focus-manager;1"]. const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; +const prefs = require("../preferences/service"); const BROWSER = 'navigator:browser', - URI_BROWSER = 'chrome://browser/content/browser.xul', + URI_BROWSER = prefs.get('browser.chromeURL', null), NAME = '_blank', FEATURES = 'chrome,all,dialog=no,non-private'; @@ -186,6 +187,9 @@ function open(uri, options) { uri = uri || URI_BROWSER; options = options || {}; + if (!uri) + throw new Error('browser.chromeURL is undefined, please provide an explicit uri'); + if (['chrome', 'resource', 'data'].indexOf(io.newURI(uri, null, null).scheme) < 0) throw new Error('only chrome, resource and data uris are allowed'); diff --git a/addon-sdk/source/lib/sdk/windows/firefox.js b/addon-sdk/source/lib/sdk/windows/firefox.js index 4e6068b9ae62..a34e2870584c 100644 --- a/addon-sdk/source/lib/sdk/windows/firefox.js +++ b/addon-sdk/source/lib/sdk/windows/firefox.js @@ -9,7 +9,6 @@ const { Cc, Ci, Cr } = require('chrome'), { EventEmitter } = require('../deprecated/events'), { WindowTabs, WindowTabTracker } = require('./tabs-firefox'), { WindowDom } = require('./dom'), - { WindowLoader } = require('./loader'), { isBrowser, getWindowDocShell, isFocused, windows: windowIterator, isWindowPrivate } = require('../window/utils'), { Options } = require('../tabs/common'), @@ -23,7 +22,10 @@ const { windowNS } = require('../window/namespace'); const { isPrivateBrowsingSupported } = require('../self'); const { ignoreWindow, isPrivate } = require('sdk/private-browsing/utils'); const { viewFor } = require('../view/core'); - +const { openDialog } = require('../window/utils'); +const ON_LOAD = 'load', + ON_UNLOAD = 'unload', + STATE_LOADED = 'complete'; /** * Window trait composes safe wrappers for browser window that are E10S * compatible. @@ -33,12 +35,96 @@ const BrowserWindowTrait = Trait.compose( WindowDom.resolve({ close: '_close' }), WindowTabs, WindowTabTracker, - WindowLoader, /* WindowSidebars, */ Trait.compose({ _emit: Trait.required, _close: Trait.required, - _load: Trait.required, + /** + * Private window who's load event is being tracked. Once window is loaded + * `_onLoad` is called. + * @type {nsIWindow} + */ + get _window() this.__window, + set _window(window) { + let _window = this.__window; + if (!window) window = null; + + if (window !== _window) { + if (_window) { + if (this.__unloadListener) + _window.removeEventListener(ON_UNLOAD, this.__unloadListener, false); + + if (this.__loadListener) + _window.removeEventListener(ON_LOAD, this.__loadListener, false); + } + + if (window) { + window.addEventListener( + ON_UNLOAD, + this.__unloadListener || + (this.__unloadListener = this._unloadListener.bind(this)) + , + false + ); + + this.__window = window; + + // If window is not loaded yet setting up a listener. + if (STATE_LOADED != window.document.readyState) { + window.addEventListener( + ON_LOAD, + this.__loadListener || + (this.__loadListener = this._loadListener.bind(this)) + , + false + ); + } + else { // If window is loaded calling listener next turn of event loop. + this._onLoad(window) + } + } + else { + this.__window = null; + } + } + }, + __window: null, + /** + * Internal method used for listening 'load' event on the `_window`. + * Method takes care of removing itself from 'load' event listeners once + * event is being handled. + */ + _loadListener: function _loadListener(event) { + let window = this._window; + if (!event.target || event.target.defaultView != window) return; + window.removeEventListener(ON_LOAD, this.__loadListener, false); + this._onLoad(window); + }, + __loadListener: null, + /** + * Internal method used for listening 'unload' event on the `_window`. + * Method takes care of removing itself from 'unload' event listeners once + * event is being handled. + */ + _unloadListener: function _unloadListener(event) { + let window = this._window; + if (!event.target + || event.target.defaultView != window + || STATE_LOADED != window.document.readyState + ) return; + window.removeEventListener(ON_UNLOAD, this.__unloadListener, false); + this._onUnload(window); + }, + __unloadListener: null, + _load: function _load() { + if (this.__window) + return; + + this._window = openDialog({ + private: this._isPrivate, + args: this._tabOptions.map(function(options) options.url).join("|") + }); + }, /** * Constructor returns wrapper of the specified chrome window. * @param {nsIWindow} window diff --git a/addon-sdk/source/lib/sdk/windows/loader.js b/addon-sdk/source/lib/sdk/windows/loader.js deleted file mode 100644 index eef2eb4e69be..000000000000 --- a/addon-sdk/source/lib/sdk/windows/loader.js +++ /dev/null @@ -1,128 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -'use strict'; - -module.metadata = { - "stability": "unstable" -}; - -const { Cc, Ci } = require('chrome'), - { setTimeout } = require('../timers'), - { Trait } = require('../deprecated/traits'), - { openDialog } = require('../window/utils'), - - ON_LOAD = 'load', - ON_UNLOAD = 'unload', - STATE_LOADED = 'complete'; - -/** - * Trait provides private `_window` property and requires `_onLoad` property - * that will be called when `_window` is loaded. If `_window` property value - * is changed with already loaded window `_onLoad` still will be called. - */ -const WindowLoader = Trait.compose({ - /** - * Internal listener that is called when window is loaded. - * Please keep in mind that this trait will not handle exceptions that may - * be thrown by this method so method itself should take care of - * handling them. - * @param {nsIWindow} window - */ - _onLoad: Trait.required, - _tabOptions: Trait.required, - /** - * Internal listener that is called when `_window`'s DOM 'unload' event - * is dispatched. Please note that this trait will not handle exceptions that - * may be thrown by this method so method itself should take care of - * handling them. - */ - _onUnload: Trait.required, - _load: function _load() { - if (this.__window) - return; - - this._window = openDialog({ - private: this._isPrivate, - args: this._tabOptions.map(function(options) options.url).join("|") - }); - }, - /** - * Private window who's load event is being tracked. Once window is loaded - * `_onLoad` is called. - * @type {nsIWindow} - */ - get _window() this.__window, - set _window(window) { - let _window = this.__window; - if (!window) window = null; - - if (window !== _window) { - if (_window) { - if (this.__unloadListener) - _window.removeEventListener(ON_UNLOAD, this.__unloadListener, false); - - if (this.__loadListener) - _window.removeEventListener(ON_LOAD, this.__loadListener, false); - } - - if (window) { - window.addEventListener( - ON_UNLOAD, - this.__unloadListener || - (this.__unloadListener = this._unloadListener.bind(this)) - , - false - ); - - this.__window = window; - - // If window is not loaded yet setting up a listener. - if (STATE_LOADED != window.document.readyState) { - window.addEventListener( - ON_LOAD, - this.__loadListener || - (this.__loadListener = this._loadListener.bind(this)) - , - false - ); - } - else { // If window is loaded calling listener next turn of event loop. - this._onLoad(window) - } - } - else { - this.__window = null; - } - } - }, - __window: null, - /** - * Internal method used for listening 'load' event on the `_window`. - * Method takes care of removing itself from 'load' event listeners once - * event is being handled. - */ - _loadListener: function _loadListener(event) { - let window = this._window; - if (!event.target || event.target.defaultView != window) return; - window.removeEventListener(ON_LOAD, this.__loadListener, false); - this._onLoad(window); - }, - __loadListener: null, - /** - * Internal method used for listening 'unload' event on the `_window`. - * Method takes care of removing itself from 'unload' event listeners once - * event is being handled. - */ - _unloadListener: function _unloadListener(event) { - let window = this._window; - if (!event.target - || event.target.defaultView != window - || STATE_LOADED != window.document.readyState - ) return; - window.removeEventListener(ON_UNLOAD, this.__unloadListener, false); - this._onUnload(window); - }, - __unloadListener: null -}); -exports.WindowLoader = WindowLoader; diff --git a/addon-sdk/source/lib/sdk/windows/observer.js b/addon-sdk/source/lib/sdk/windows/observer.js index d8336464c9cf..512d57b94149 100644 --- a/addon-sdk/source/lib/sdk/windows/observer.js +++ b/addon-sdk/source/lib/sdk/windows/observer.js @@ -7,19 +7,29 @@ module.metadata = { "stability": "unstable" }; -const { EventEmitterTrait: EventEmitter } = require("../deprecated/events"); +const { EventTarget } = require("../event/target"); +const { emit } = require("../event/core"); const { WindowTracker, windowIterator } = require("../deprecated/window-utils"); const { DOMEventAssembler } = require("../deprecated/events/assembler"); -const { Trait } = require("../deprecated/light-traits"); +const { Class } = require("../core/heritage"); // Event emitter objects used to register listeners and emit events on them // when they occur. -const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ - /** - * Method is implemented by `EventEmitter` and is used just for emitting - * events on registered listeners. - */ - _emit: Trait.required, +const Observer = Class({ + initialize() { + // Using `WindowTracker` to track window events. + WindowTracker({ + onTrack: chromeWindow => { + emit(this, "open", chromeWindow); + this.observe(chromeWindow); + }, + onUntrack: chromeWindow => { + emit(this, "close", chromeWindow); + this.ignore(chromeWindow); + } + }); + }, + implements: [EventTarget, DOMEventAssembler], /** * Events that are supported and emitted by the module. */ @@ -31,21 +41,9 @@ const observer = Trait.compose(DOMEventAssembler, EventEmitter).create({ * @param {Event} event * Keyboard event being emitted. */ - handleEvent: function handleEvent(event) { - this._emit(event.type, event.target, event); + handleEvent(event) { + emit(this, event.type, event.target, event); } }); -// Using `WindowTracker` to track window events. -WindowTracker({ - onTrack: function onTrack(chromeWindow) { - observer._emit("open", chromeWindow); - observer.observe(chromeWindow); - }, - onUntrack: function onUntrack(chromeWindow) { - observer._emit("close", chromeWindow); - observer.ignore(chromeWindow); - } -}); - -exports.observer = observer; +exports.observer = new Observer(); diff --git a/addon-sdk/source/lib/sdk/windows/tabs-fennec.js b/addon-sdk/source/lib/sdk/windows/tabs-fennec.js index db18213142af..080b727d3162 100644 --- a/addon-sdk/source/lib/sdk/windows/tabs-fennec.js +++ b/addon-sdk/source/lib/sdk/windows/tabs-fennec.js @@ -158,7 +158,7 @@ function onTabSelect(event) { emit(tab, 'activate', tab); emit(gTabs, 'activate', tab); - for (let of in gTabs) { + for (let t of gTabs) { if (t === tab) continue; emit(t, 'deactivate', t); emit(gTabs, 'deactivate', t); diff --git a/addon-sdk/source/lib/toolkit/loader.js b/addon-sdk/source/lib/toolkit/loader.js index 70a89b1c46f4..dcbce70d571f 100644 --- a/addon-sdk/source/lib/toolkit/loader.js +++ b/addon-sdk/source/lib/toolkit/loader.js @@ -2,28 +2,20 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -;(function(id, factory) { // Module boilerplate :( - if (typeof(define) === 'function') { // RequireJS - define(factory); - } else if (typeof(require) === 'function') { // CommonJS - factory.call(this, require, exports, module); - } else if (~String(this).indexOf('BackstagePass')) { // JSM - this[factory.name] = {}; - factory(function require(uri) { - var imports = {}; - this['Components'].utils.import(uri, imports); - return imports; - }, this[factory.name], { uri: __URI__, id: id }); - this.EXPORTED_SYMBOLS = [factory.name]; - } else if (~String(this).indexOf('Sandbox')) { // Sandbox - factory(function require(uri) {}, this, { uri: __URI__, id: id }); - } else { // Browser or alike - var globals = this - factory(function require(id) { - return globals[id]; - }, (globals[id] = {}), { uri: document.location.href + '#' + id, id: id }); +;((factory) => { // Module boilerplate :( + if (typeof(require) === 'function') { // CommonJS + require("chrome").Cu.import(module.uri, exports); } -}).call(this, 'loader', function Loader(require, exports, module) { + else if (~String(this).indexOf('BackstagePass')) { // JSM + let module = { uri: __URI__, id: "toolkit/loader", exports: Object.create(null) } + factory(module); + Object.assign(this, module.exports); + this.EXPORTED_SYMBOLS = Object.getOwnPropertyNames(module.exports); + } + else { + throw Error("Loading environment is not supported"); + } +})(module => { 'use strict'; @@ -38,11 +30,16 @@ const { loadSubScript } = Cc['@mozilla.org/moz/jssubscript-loader;1']. getService(Ci.mozIJSSubScriptLoader); const { notifyObservers } = Cc['@mozilla.org/observer-service;1']. getService(Ci.nsIObserverService); +const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); -const { Reflect } = Cu.import("resource://gre/modules/reflect.jsm", {}); -const { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm"); const { join: pathJoin, normalize, dirname } = Cu.import("resource://gre/modules/osfile/ospath_unix.jsm"); +XPCOMUtils.defineLazyGetter(this, "XulApp", () => { + let xulappURI = module.uri.replace("toolkit/loader.js", + "sdk/system/xul-app.jsm"); + return Cu.import(xulappURI, {}); +}); + // Define some shortcuts. const bind = Function.call.bind(Function.bind); const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; @@ -90,7 +87,7 @@ const descriptor = iced(function descriptor(object) { }); return value; }); -exports.descriptor = descriptor; +Loader.descriptor = descriptor; // Freeze important built-ins so they can't be used by untrusted code as a // message passing channel. @@ -127,15 +124,15 @@ const override = iced(function override(target, source) { }); return define({}, properties); }); -exports.override = override; +Loader.override = override; function sourceURI(uri) { return String(uri).split(" -> ").pop(); } -exports.sourceURI = iced(sourceURI); +Loader.sourceURI = iced(sourceURI); function isntLoaderFrame(frame) { return frame.fileName !== module.uri } function parseURI(uri) { return String(uri).split(" -> ").pop(); } -exports.parseURI = parseURI; +Loader.parseURI = parseURI; function parseStack(stack) { let lines = String(stack).split("\n"); @@ -158,7 +155,7 @@ function parseStack(stack) { return frames; }, []); } -exports.parseStack = parseStack; +Loader.parseStack = parseStack; function serializeStack(frames) { return frames.reduce(function(stack, frame) { @@ -169,7 +166,7 @@ function serializeStack(frames) { stack; }, ""); } -exports.serializeStack = serializeStack; +Loader.serializeStack = serializeStack; function readURI(uri) { let stream = NetUtil.newChannel2(uri, @@ -201,7 +198,7 @@ function join (...paths) { resolved = resolved.replace(/^chrome\:\/([^\/])/, 'chrome://$1'); return resolved; } -exports.join = join; +Loader.join = join; // Function takes set of options and returns a JS sandbox. Function may be // passed set of options: @@ -253,7 +250,7 @@ const Sandbox = iced(function Sandbox(options) { return sandbox; }); -exports.Sandbox = Sandbox; +Loader.Sandbox = Sandbox; // Evaluates code from the given `uri` into given `sandbox`. If // `options.source` is passed, then that code is evaluated instead. @@ -273,7 +270,7 @@ const evaluate = iced(function evaluate(sandbox, uri, options) { return source ? Cu.evalInSandbox(source, sandbox, version, uri, line) : loadSubScript(uri, sandbox, encoding); }); -exports.evaluate = evaluate; +Loader.evaluate = evaluate; // Populates `exports` of the given CommonJS `module` object, in the context // of the given `loader` by evaluating code associated with it. @@ -306,7 +303,8 @@ const load = iced(function load(loader, module) { descriptors[name] = getOwnPropertyDescriptor(globals, name) }); define(sandbox, descriptors); - } else { + } + else { sandbox = Sandbox({ name: module.uri, prototype: create(globals, descriptors), @@ -323,7 +321,8 @@ const load = iced(function load(loader, module) { try { evaluate(sandbox, module.uri); - } catch (error) { + } + catch (error) { let { message, fileName, lineNumber } = error; let stack = error.stack || Error().stack; let frames = parseStack(stack).filter(isntLoaderFrame); @@ -361,12 +360,19 @@ const load = iced(function load(loader, module) { }); } + if (loader.checkCompatibility) { + let err = XulApp.incompatibility(module); + if (err) { + throw err; + } + } + if (module.exports && typeof(module.exports) === 'object') freeze(module.exports); return module; }); -exports.load = load; +Loader.load = load; // Utility function to normalize module `uri`s so they have `.js` extension. function normalizeExt (uri) { @@ -403,7 +409,7 @@ const resolve = iced(function resolve(id, base) { return resolved; }); -exports.resolve = resolve; +Loader.resolve = resolve; // Node-style module lookup // Takes an id and path and attempts to load a file using node's resolving @@ -412,7 +418,7 @@ exports.resolve = resolve; // http://nodejs.org/api/modules.html#modules_all_together const nodeResolve = iced(function nodeResolve(id, requirer, { rootURI }) { // Resolve again - id = exports.resolve(id, requirer); + id = Loader.resolve(id, requirer); // we assume that extensions are correct, i.e., a directory doesnt't have '.js' // and a js file isn't named 'file.json.js' @@ -441,7 +447,7 @@ const nodeResolve = iced(function nodeResolve(id, requirer, { rootURI }) { // with `resolveURI` -- if during runtime, then `resolve` will throw. return void 0; }); -exports.nodeResolve = nodeResolve; +Loader.nodeResolve = nodeResolve; // Attempts to load `path` and then `path.js` // Returns `path` with valid file, or `undefined` otherwise @@ -538,7 +544,7 @@ const resolveURI = iced(function resolveURI(id, mapping) { } return void 0; // otherwise we raise a warning, see bug 910304 }); -exports.resolveURI = resolveURI; +Loader.resolveURI = resolveURI; // Creates version of `require` that will be exposed to the given `module` // in the context of the given `loader`. Each module gets own limited copy @@ -652,7 +658,7 @@ const Require = iced(function Require(loader, requirer) { // found in the paths most likely, like `sdk/tabs`, which should // be resolved relatively if needed using traditional resolve if (!requirement) { - requirement = isRelative(id) ? exports.resolve(id, requirer.id) : id; + requirement = isRelative(id) ? Loader.resolve(id, requirer.id) : id; } } else { // Resolve `id` to its requirer if it's relative. @@ -679,7 +685,7 @@ const Require = iced(function Require(loader, requirer) { require.main = loader.main === requirer ? requirer : undefined; return iced(require); }); -exports.Require = Require; +Loader.Require = Require; const main = iced(function main(loader, id) { // If no main entry provided, and native loader is used, @@ -690,7 +696,7 @@ const main = iced(function main(loader, id) { let module = loader.main = loader.modules[uri] = Module(id, uri); return loader.load(loader, module).exports; }); -exports.main = main; +Loader.main = main; // Makes module object that is made available to CommonJS modules when they // are evaluated, along with `exports` and `require`. @@ -701,7 +707,7 @@ const Module = iced(function Module(id, uri) { uri: { value: uri } }); }); -exports.Module = Module; +Loader.Module = Module; // Takes `loader`, and unload `reason` string and notifies all observers that // they should cleanup after them-self. @@ -716,7 +722,7 @@ const unload = iced(function unload(loader, reason) { let subject = { wrappedJSObject: loader.destructor }; notifyObservers(subject, 'sdk:loader:destroy', reason); }); -exports.unload = unload; +Loader.unload = unload; // Function makes new loader that can be used to load CommonJS modules // described by a given `options.manifest`. Loader takes following options: @@ -731,24 +737,29 @@ exports.unload = unload; // module object (that has `uri` property) and `baseURI` of the loader. // If `resolve` does not returns `uri` string exception will be thrown by // an associated `require` call. -const Loader = iced(function Loader(options) { - let console = new ConsoleAPI({ - consoleID: options.id ? "addon/" + options.id : "" - }); - +function Loader(options) { let { modules, globals, resolve, paths, rootURI, manifest, requireMap, isNative, - metadata, sharedGlobal, sharedGlobalBlacklist + metadata, sharedGlobal, sharedGlobalBlacklist, checkCompatibility } = override({ paths: {}, modules: {}, globals: { - console: console + get console() { + // Import Console.jsm from here to prevent loading it until someone uses it + let { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm"); + let console = new ConsoleAPI({ + consoleID: options.id ? "addon/" + options.id : "" + }); + Object.defineProperty(this, "console", { value: console }); + return this.console; + } }, + checkCompatibility: false, resolve: options.isNative ? // Make the returned resolve function have the same signature - (id, requirer) => exports.nodeResolve(id, requirer, { rootURI: rootURI }) : - exports.resolve, + (id, requirer) => Loader.nodeResolve(id, requirer, { rootURI: rootURI }) : + Loader.resolve, sharedGlobalBlacklist: ["sdk/indexed-db"] }, options); @@ -825,6 +836,7 @@ const Loader = iced(function Loader(options) { invisibleToDebugger: { enumerable: false, value: options.invisibleToDebugger || false }, load: { enumerable: false, value: options.load || load }, + checkCompatibility: { enumerable: false, value: checkCompatibility }, // Main (entry point) module, it can be set only once, since loader // instance can have only one main module. main: new function() { @@ -846,8 +858,8 @@ const Loader = iced(function Loader(options) { } return freeze(create(null, returnObj)); -}); -exports.Loader = Loader; +}; +Loader.Loader = Loader; let isJSONURI = uri => uri.substr(-5) === '.json'; let isJSMURI = uri => uri.substr(-4) === '.jsm'; @@ -860,7 +872,7 @@ let isRelative = id => id[0] === '.' const generateMap = iced(function generateMap(options, callback) { let { rootURI, resolve, paths } = override({ paths: {}, - resolve: exports.nodeResolve + resolve: Loader.nodeResolve }, options); rootURI = addTrailingSlash(rootURI); @@ -882,7 +894,7 @@ const generateMap = iced(function generateMap(options, callback) { }, {}, callback); }); -exports.generateMap = generateMap; +Loader.generateMap = generateMap; // Default `main` entry to './index.js' and ensure is relative, // since node allows 'lib/index.js' without relative `./` @@ -950,6 +962,8 @@ function findModuleIncludes (uri, callback) { } function walk (src, callback) { + // Import Reflect.jsm from here to prevent loading it until someone uses it + let { Reflect } = Cu.import("resource://gre/modules/reflect.jsm", {}); let nodes = Reflect.parse(src); traverse(nodes, callback); } @@ -988,4 +1002,5 @@ function isRequire (node) { && node.arguments[0].type === 'Literal'; } +module.exports = iced(Loader); }); diff --git a/addon-sdk/source/lib/toolkit/require.js b/addon-sdk/source/lib/toolkit/require.js index 27b4e1c71970..ed1a36788132 100644 --- a/addon-sdk/source/lib/toolkit/require.js +++ b/addon-sdk/source/lib/toolkit/require.js @@ -1,3 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + const make = (exports, rootURI, components) => { const { Loader: { Loader, Require, Module, main } } = components.utils.import(rootURI + "toolkit/loader.js", {}); @@ -12,16 +16,49 @@ const make = (exports, rootURI, components) => { } }); + // Implement require.unload(uri) that can be used to unload + // already loaded module which is convinient during development phase. + const unload = uri => { + delete loader.sandboxes[uri]; + delete loader.modules[uri]; + }; + + const builtins = new Set(Object.keys(loader.modules)); + // Below we define `require` & `require.resolve` that resolve passed // module id relative to the caller URI. This is not perfect but good // enough for common case & there is always an option to pass absolute // id when that // but presumably well enough to cover - const require = id => { + const require = (id, options={}) => { + const { reload, all } = options; const requirerURI = components.stack.caller.filename; const requirer = Module(requirerURI, requirerURI); - return Require(loader, requirer)(id); + const require = Require(loader, requirer); + if (reload) { + // To load JS code into modules, loader uses `mozIJSSubScriptLoader` + // which uses startup cache to avoid reading source from the same URI + // more than once. Unless we invalidate statup cache changes to a module + // won't be reflected even after reload. Therefor we must dispatch an + // nsIObserverService notification that causes cache invalidation. + // Note: This is not ideal since it destroys whole cache, but since there + // is no way to invalidate individual entries, we assume performance hit + // during development is acceptable. + components.classes["@mozilla.org/observer-service;1"]. + getService(components.interfaces.nsIObserverService). + notifyObservers({}, "startupcache-invalidate", null); + + if (all) { + for (let uri of Object.keys(loader.sandboxes)) { + unload(uri); + } + } + else { + unload(require.resolve(id)); + } + } + return require(id); }; require.resolve = id => { diff --git a/addon-sdk/source/modules/system/Startup.js b/addon-sdk/source/modules/system/Startup.js index 3d5a7e861e19..7764090771ce 100644 --- a/addon-sdk/source/modules/system/Startup.js +++ b/addon-sdk/source/modules/system/Startup.js @@ -7,9 +7,10 @@ var EXPORTED_SYMBOLS = ["Startup"]; const { utils: Cu, interfaces: Ci, classes: Cc } = Components; const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); -const { XulApp } = Cu.import("resource://gre/modules/sdk/system/XulApp.js", {}); const { defer } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; +const { XulApp } = Cu.import("resource://gre/modules/commonjs/sdk/system/xul-app.jsm", {}); + const appStartupSrv = Cc["@mozilla.org/toolkit/app-startup;1"] .getService(Ci.nsIAppStartup); diff --git a/addon-sdk/source/modules/system/moz.build b/addon-sdk/source/modules/system/moz.build index 3b068513150f..ee811f14adff 100644 --- a/addon-sdk/source/modules/system/moz.build +++ b/addon-sdk/source/modules/system/moz.build @@ -6,5 +6,4 @@ EXTRA_JS_MODULES.sdk.system += [ 'Startup.js', - 'XulApp.js', ] diff --git a/addon-sdk/source/package.json b/addon-sdk/source/package.json index 5f960a521abb..fa96ed265fc5 100644 --- a/addon-sdk/source/package.json +++ b/addon-sdk/source/package.json @@ -1,11 +1,36 @@ { - "name": "addon-sdk", - "description": "Add-on development made easy.", - "keywords": [ - "javascript", "engine", "addon", "extension", - "xulrunner", "firefox", "browser" - ], - "loader": "lib/sdk/loader/cuddlefish.js", - "license": "MPL 2.0", - "unpack": true + "name": "addon-sdk", + "description": "Add-on development made easy.", + "keywords": [ + "javascript", "engine", "addon", "extension", + "xulrunner", "firefox", "browser" + ], + "license": "MPL 2.0", + "unpack": true, + "scripts": { + "test": "node ./bin/jpm-test.js", + "modules": "node ./bin/jpm-test.js --type modules", + "addons": "node ./bin/jpm-test.js --type addons", + "examples": "node ./bin/jpm-test.js --type examples" + }, + "homepage": "https://github.com/mozilla/addon-sdk", + "repository": { + "type": "git", + "url": "git://github.com/mozilla/addon-sdk.git" + }, + "version": "0.1.18", + "main": "./lib/index.js", + "loader": "lib/sdk/loader/cuddlefish.js", + "devDependencies": { + "async": "0.2.10", + "chai": "1.9.2", + "glob": "4.0.6", + "jpm": "0.0.23", + "lodash": "2.4.1", + "mocha": "1.21.5", + "promise": "6.0.1", + "rimraf": "2.2.8", + "unzip": "0.1.9", + "xmldom": "0.1.19" + } } diff --git a/addon-sdk/source/python-lib/cuddlefish/__init__.py b/addon-sdk/source/python-lib/cuddlefish/__init__.py index 7fda2003d596..7fafe14689b1 100644 --- a/addon-sdk/source/python-lib/cuddlefish/__init__.py +++ b/addon-sdk/source/python-lib/cuddlefish/__init__.py @@ -861,8 +861,7 @@ def run(arguments=sys.argv[1:], target_cfg=None, pkg_cfg=None, jid=jid, update_url=options.update_url, bootstrap=True, - enable_mobile=options.enable_mobile, - harness_options=harness_options) + enable_mobile=options.enable_mobile) if command == "xpi" and options.update_link: if not options.update_link.startswith("https"): diff --git a/addon-sdk/source/python-lib/cuddlefish/manifest.py b/addon-sdk/source/python-lib/cuddlefish/manifest.py index c6d23fb31059..e9913be7bbb1 100644 --- a/addon-sdk/source/python-lib/cuddlefish/manifest.py +++ b/addon-sdk/source/python-lib/cuddlefish/manifest.py @@ -417,6 +417,9 @@ class ManifestBuilder: # test-securable-module.js, and the modules/red.js # that it imports, both do that intentionally continue + if reqname.endswith(".jsm"): + # ignore JSM modules + continue if not self.abort_on_missing: # print a warning, but tolerate missing modules # unless cfx --abort-on-missing-module flag was set @@ -802,4 +805,3 @@ if __name__ == '__main__': sys.exit(1) print "requires: %s" % (",".join(sorted(requires.keys()))) print "locations: %s" % locations - diff --git a/addon-sdk/source/python-lib/cuddlefish/packaging.py b/addon-sdk/source/python-lib/cuddlefish/packaging.py index ab38f1034244..0c5357e8e977 100644 --- a/addon-sdk/source/python-lib/cuddlefish/packaging.py +++ b/addon-sdk/source/python-lib/cuddlefish/packaging.py @@ -84,7 +84,7 @@ def validate_resource_hostname(name): """ # See https://bugzilla.mozilla.org/show_bug.cgi?id=568131 for details. - if not name.islower(): + if not name.lower() == name: raise ValueError("""Error: the name of your package contains upper-case letters. Package names can contain only lower-case letters, numbers, underscores, and dashes. Current package name: %s""" % name) diff --git a/addon-sdk/source/python-lib/cuddlefish/prefs.py b/addon-sdk/source/python-lib/cuddlefish/prefs.py index bbee9ef0ae85..8a311110ad83 100644 --- a/addon-sdk/source/python-lib/cuddlefish/prefs.py +++ b/addon-sdk/source/python-lib/cuddlefish/prefs.py @@ -36,17 +36,23 @@ DEFAULT_COMMON_PREFS = { # shut up some warnings on `about:` page 'app.releaseNotesURL': 'http://localhost/app-dummy/', - 'app.vendorURL': 'http://localhost/app-dummy/' + 'app.vendorURL': 'http://localhost/app-dummy/', + + # Don't prompt about e10s + 'browser.displayedE10SPrompt.1': 5 } DEFAULT_NO_CONNECTIONS_PREFS = { 'toolkit.telemetry.enabled': False, + 'toolkit.telemetry.server': 'https://localhost/telemetry-dummy/', 'app.update.auto' : False, 'app.update.url': 'http://localhost/app-dummy/update', + # Make sure GMPInstallManager won't hit the network. 'media.gmp-gmpopenh264.autoupdate' : False, 'media.gmp-manager.cert.checkAttributes' : False, 'media.gmp-manager.cert.requireBuiltIn' : False, 'media.gmp-manager.url' : 'http://localhost/media-dummy/gmpmanager', + 'media.gmp-manager.url.override': 'http://localhost/dummy-gmp-manager.xml', 'browser.newtab.url' : 'about:blank', 'browser.search.update': False, 'browser.safebrowsing.enabled' : False, @@ -54,9 +60,12 @@ DEFAULT_NO_CONNECTIONS_PREFS = { 'browser.safebrowsing.gethashURL': 'http://localhost/safebrowsing-dummy/gethash', 'browser.safebrowsing.reportURL': 'http://localhost/safebrowsing-dummy/report', 'browser.safebrowsing.malware.reportURL': 'http://localhost/safebrowsing-dummy/malwarereport', + 'browser.trackingprotection.gethashURL': 'http://localhost/safebrowsing-dummy/gethash', + 'browser.trackingprotection.updateURL': 'http://localhost/safebrowsing-dummy/update', # Disable app update 'app.update.enabled' : False, + 'app.update.staging.enabled': False, # Disable about:newtab content fetch and ping 'browser.newtabpage.directory.source': 'data:application/json,{"jetpack":1}', @@ -64,9 +73,31 @@ DEFAULT_NO_CONNECTIONS_PREFS = { # Point update checks to a nonexistent local URL for fast failures. 'extensions.update.url' : 'http://localhost/extensions-dummy/updateURL', + 'extensions.update.background.url': 'http://localhost/extensions-dummy/updateBackgroundURL', 'extensions.blocklist.url' : 'http://localhost/extensions-dummy/blocklistURL', # Make sure opening about:addons won't hit the network. - 'extensions.webservice.discoverURL' : 'http://localhost/extensions-dummy/discoveryURL' + 'extensions.webservice.discoverURL' : 'http://localhost/extensions-dummy/discoveryURL', + 'extensions.getAddons.maxResults': 0, + + # Disable webapp updates. Yes, it is supposed to be an integer. + 'browser.webapps.checkForUpdates': 0, + + # Location services + 'geo.wifi.uri': 'http://localhost/location-dummy/locationURL', + 'browser.search.geoip.url': 'http://localhost/location-dummy/locationURL', + + # Tell the search service we are running in the US. This also has the + # desired side-effect of preventing our geoip lookup. + 'browser.search.isUS' : True, + 'browser.search.countryCode' : 'US', + + 'geo.wifi.uri' : 'http://localhost/extensions-dummy/geowifiURL', + 'geo.wifi.scan' : False, + + # We don't want to hit the real Firefox Accounts server for tests. We don't + # actually need a functioning FxA server, so just set it to something that + # resolves and accepts requests, even if they all fail. + 'identity.fxaccounts.auth.uri': 'http://localhost/fxa-dummy/' } DEFAULT_FENNEC_PREFS = { @@ -78,6 +109,7 @@ DEFAULT_FENNEC_PREFS = { DEFAULT_FIREFOX_PREFS = { 'browser.startup.homepage' : 'about:blank', 'startup.homepage_welcome_url' : 'about:blank', + 'devtools.browsertoolbox.panel': 'jsdebugger', 'devtools.errorconsole.enabled' : True, 'devtools.chrome.enabled' : True, @@ -145,6 +177,69 @@ DEFAULT_THUNDERBIRD_PREFS = { } DEFAULT_TEST_PREFS = { + 'browser.console.showInPanel': True, + 'browser.startup.page': 0, + 'browser.firstrun.show.localepicker': False, + 'browser.firstrun.show.uidiscovery': False, + 'browser.ui.layout.tablet': 0, + 'dom.disable_open_during_load': False, + 'dom.experimental_forms': True, + 'dom.forms.number': True, + 'dom.forms.color': True, + 'dom.max_script_run_time': 0, + 'hangmonitor.timeout': 0, + 'dom.max_chrome_script_run_time': 0, + 'dom.popup_maximum': -1, + 'dom.send_after_paint_to_content': True, + 'dom.successive_dialog_time_limit': 0, + 'browser.shell.checkDefaultBrowser': False, + 'shell.checkDefaultClient': False, + 'browser.warnOnQuit': False, + 'accessibility.typeaheadfind.autostart': False, + 'browser.EULA.override': True, + 'gfx.color_management.force_srgb': True, + 'network.manage-offline-status': False, + # Disable speculative connections so they aren't reported as leaking when they're hanging around. + 'network.http.speculative-parallel-limit': 0, + 'test.mousescroll': True, + # Need to client auth test be w/o any dialogs + 'security.default_personal_cert': 'Select Automatically', + 'network.http.prompt-temp-redirect': False, + 'security.warn_viewing_mixed': False, + 'browser.panorama.experienced_first_run': True, + # Set a future policy version to avoid the telemetry prompt. + 'toolkit.telemetry.prompted': 999, + 'toolkit.telemetry.notifiedOptOut': 999, + 'extensions.defaultProviders.enabled': True, + 'datareporting.policy.dataSubmissionPolicyBypassNotification': True, + 'layout.css.report_errors': True, + 'layout.css.grid.enabled': True, + 'layout.spammy_warnings.enabled': False, + 'dom.mozSettings.enabled': True, + # Make sure the disk cache doesn't get auto disabled + 'network.http.bypass-cachelock-threshold': 200000, + # Always use network provider for geolocation tests + # so we bypass the OSX dialog raised by the corelocation provider + 'geo.provider.testing': True, + # Background thumbnails in particular cause grief, and disabling thumbnails + # in general can't hurt - we re-enable them when tests need them. + 'browser.pagethumbnails.capturing_disabled': True, + # Indicate that the download panel has been shown once so that whichever + # download test runs first doesn't show the popup inconsistently. + 'browser.download.panel.shown': True, + # Assume the about:newtab page's intro panels have been shown to not depend on + # which test runs first and happens to open about:newtab + 'browser.newtabpage.introShown': True, + # Disable useragent updates. + 'general.useragent.updates.enabled': False, + 'dom.mozApps.debug': True, + 'dom.apps.customization.enabled': True, + 'media.eme.enabled': True, + # Don't forceably kill content processes after a timeout + 'dom.ipc.tabs.shutdownTimeoutSecs': 0, + # Don't show the search first run UI by default + 'browser.search.highlightCount': 0, 'general.useragent.locale': "en-US", - 'intl.locale.matchOS': "en-US" + 'intl.locale.matchOS': "en-US", + 'dom.indexedDB.experimental': True } diff --git a/addon-sdk/source/python-lib/cuddlefish/rdf.py b/addon-sdk/source/python-lib/cuddlefish/rdf.py index c80be82ed830..fb9bb76f6d6f 100644 --- a/addon-sdk/source/python-lib/cuddlefish/rdf.py +++ b/addon-sdk/source/python-lib/cuddlefish/rdf.py @@ -5,8 +5,6 @@ import os import xml.dom.minidom import StringIO -import codecs -import glob RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" EM_NS = "http://www.mozilla.org/2004/em-rdf#" @@ -22,10 +20,9 @@ class RDF(object): # have .encoding hardwired to "ascii" and put only bytes in # the backing store, so we can't use them here). # - # The encoding= argument to dom.writexml() merely sets the - # XML header's encoding= attribute. It still writes unencoded - # unicode to the output file, so we have to encode it for - # real afterwards. + # The encoding= argument to dom.writexml() merely sets the XML header's + # encoding= attribute. It still writes unencoded unicode to the output file, + # so we have to encode it for real afterwards. # # Also see: https://bugzilla.mozilla.org/show_bug.cgi?id=567660 @@ -115,12 +112,7 @@ class RDFManifest(RDF): return True; - def add_node(self, node): - top = self.dom.documentElement.getElementsByTagName("Description")[0]; - top.appendChild(node) - - -def gen_manifest(template_root_dir, target_cfg, jid, harness_options={}, +def gen_manifest(template_root_dir, target_cfg, jid, update_url=None, bootstrap=True, enable_mobile=False): install_rdf = os.path.join(template_root_dir, "install.rdf") manifest = RDFManifest(install_rdf) @@ -129,51 +121,13 @@ def gen_manifest(template_root_dir, target_cfg, jid, harness_options={}, manifest.set("em:id", jid) manifest.set("em:version", target_cfg.get('version', '1.0')) - - if "locale" in harness_options: - # addon_title -> - # addon_author -> - # addon_description -> - # addon_homepageURL -> - localizable_in = ["title", "author", "description", "homepage"] - localized_out = ["name", "creator", "description", "homepageURL"] - for lang in harness_options["locale"]: - desc = dom.createElement("Description") - - for value_in in localizable_in: - key_in = "extensions." + target_cfg.get("id", "") + "." + value_in - tag_out = localized_out[localizable_in.index(value_in)] - - if key_in in harness_options["locale"][lang]: - elem = dom.createElement("em:" + tag_out) - elem_value = harness_options["locale"][lang][key_in] - elem.appendChild(dom.createTextNode(elem_value)) - desc.appendChild(elem) - - # Don't add language if no localizeable field was localized - if desc.hasChildNodes(): - locale = dom.createElement("em:locale") - locale.appendChild(dom.createTextNode(lang)) - desc.appendChild(locale) - - localized = dom.createElement("em:localized") - localized.appendChild(desc) - manifest.add_node(localized) - manifest.set("em:name", target_cfg.get('title', target_cfg.get('fullName', target_cfg['name']))) manifest.set("em:description", target_cfg.get("description", "")) manifest.set("em:creator", target_cfg.get("author", "")) - - if target_cfg.get("homepage"): - manifest.set("em:homepageURL", target_cfg.get("homepage")) - else: - manifest.remove("em:homepageURL") - manifest.set("em:bootstrap", str(bootstrap).lower()) - # XPIs remain packed by default, but package.json can override that. The # RDF format accepts "true" as True, anything else as False. We expect # booleans in the .json file, not strings. @@ -182,7 +136,7 @@ def gen_manifest(template_root_dir, target_cfg, jid, harness_options={}, for translator in target_cfg.get("translators", [ ]): elem = dom.createElement("em:translator"); elem.appendChild(dom.createTextNode(translator)) - manifest.add_node(elem) + dom.documentElement.getElementsByTagName("Description")[0].appendChild(elem) for developer in target_cfg.get("developers", [ ]): elem = dom.createElement("em:developer"); @@ -192,7 +146,7 @@ def gen_manifest(template_root_dir, target_cfg, jid, harness_options={}, for contributor in target_cfg.get("contributors", [ ]): elem = dom.createElement("em:contributor"); elem.appendChild(dom.createTextNode(contributor)) - manifest.add_node(elem) + dom.documentElement.getElementsByTagName("Description")[0].appendChild(elem) if update_url: manifest.set("em:updateURL", update_url) @@ -215,7 +169,7 @@ def gen_manifest(template_root_dir, target_cfg, jid, harness_options={}, if enable_mobile: target_app = dom.createElement("em:targetApplication") - manifest.add_node(target_app) + dom.documentElement.getElementsByTagName("Description")[0].appendChild(target_app) ta_desc = dom.createElement("Description") target_app.appendChild(ta_desc) @@ -232,6 +186,11 @@ def gen_manifest(template_root_dir, target_cfg, jid, harness_options={}, elem.appendChild(dom.createTextNode("30.0a1")) ta_desc.appendChild(elem) + if target_cfg.get("homepage"): + manifest.set("em:homepageURL", target_cfg.get("homepage")) + else: + manifest.remove("em:homepageURL") + return manifest if __name__ == "__main__": diff --git a/addon-sdk/source/python-lib/cuddlefish/runner.py b/addon-sdk/source/python-lib/cuddlefish/runner.py index 6527eca14fe1..7f337cc038cb 100644 --- a/addon-sdk/source/python-lib/cuddlefish/runner.py +++ b/addon-sdk/source/python-lib/cuddlefish/runner.py @@ -418,6 +418,10 @@ def run_app(harness_root_dir, manifest_rdf, harness_options, if enable_e10s: preferences['browser.tabs.remote.autostart'] = True + else: + preferences['browser.tabs.remote.autostart'] = False + preferences['browser.tabs.remote.autostart.1'] = False + preferences['browser.tabs.remote.autostart.2'] = False # For now, only allow running on Mobile with --force-mobile argument if app_type in ["fennec-on-device"] and not enable_mobile: diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/noLocalization/locale/en-GB.properties b/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/noLocalization/locale/en-GB.properties deleted file mode 100644 index 5a57eb596826..000000000000 --- a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/noLocalization/locale/en-GB.properties +++ /dev/null @@ -1 +0,0 @@ -some_key = some_value diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/noLocalization/locale/en-US.properties b/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/noLocalization/locale/en-US.properties deleted file mode 100644 index 5a57eb596826..000000000000 --- a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/noLocalization/locale/en-US.properties +++ /dev/null @@ -1 +0,0 @@ -some_key = some_value diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/noLocalization/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/noLocalization/package.json deleted file mode 100644 index 5b0bb85e358a..000000000000 --- a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/noLocalization/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "nolocalization", - "id": "jid1-TBF7sWF7yT6xSQ", - "license": "MPL 2.0", - "version": "0.1", - - "title": "tilteUnlocalized", - "author": "authorUnlocalized", - "description": "descriptionUnlocalized", - "homepage": "homepageUnlocalized" -} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/twoLanguages/locale/en-GB.properties b/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/twoLanguages/locale/en-GB.properties deleted file mode 100644 index 64d08ab8bf44..000000000000 --- a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/twoLanguages/locale/en-GB.properties +++ /dev/null @@ -1,4 +0,0 @@ -extensions.jid1-TBF7sWF7yT6xSQ@jetpack.title = title-en-GB -extensions.jid1-TBF7sWF7yT6xSQ@jetpack.author = author-en-GB -extensions.jid1-TBF7sWF7yT6xSQ@jetpack.description = description-en-GB -extensions.jid1-TBF7sWF7yT6xSQ@jetpack.homepage = homepage-en-GB diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/twoLanguages/locale/en-US.properties b/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/twoLanguages/locale/en-US.properties deleted file mode 100644 index a742254ec5d3..000000000000 --- a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/twoLanguages/locale/en-US.properties +++ /dev/null @@ -1,4 +0,0 @@ -extensions.jid1-TBF7sWF7yT6xSQ@jetpack.title = title-en-US -extensions.jid1-TBF7sWF7yT6xSQ@jetpack.author = author-en-US -extensions.jid1-TBF7sWF7yT6xSQ@jetpack.description = description-en-US -extensions.jid1-TBF7sWF7yT6xSQ@jetpack.homepage = homepage-en-US diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/twoLanguages/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/twoLanguages/package.json deleted file mode 100644 index e6a2f31946d0..000000000000 --- a/addon-sdk/source/python-lib/cuddlefish/tests/bug-661083-files/packages/twoLanguages/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "nolocalization", - "id": "jid1-TBF7sWF7yT6xSQ@jetpack", - "license": "MPL 2.0", - "version": "0.1", - - "title": "tilteUnlocalized", - "author": "authorUnlocalized", - "description": "descriptionUnlocalized", - "homepage": "homepageUnlocalized" -} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_licenses.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_licenses.py index 8acb259e1b95..574812ada4e1 100644 --- a/addon-sdk/source/python-lib/cuddlefish/tests/test_licenses.py +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_licenses.py @@ -44,19 +44,28 @@ class Licenses(unittest.TestCase): skipdirs=["sdk-docs"], # test_generate.py makes this ) self.scan(os.path.join("python-lib", "mozrunner"), [".py"]) - - for sdk_package in ["addon-kit", "api-utils", "test-harness"]: - self.scan(os.path.join("packages", sdk_package), - [".js", ".py", ".md"]) + self.scan("lib", [".js", ".jsm", ".css"], + skipdirs=[ + "diffpatcher", # MIT + "method", # MIT + "child_process", # MPL 1.1/GPL 2.0/LGPL 2.1 + "fs" # MIT + ]) + self.scan("test", [".js", ".jsm", ".css", ".html"], + skipdirs=[ + "buffers", # MIT + "querystring", # MIT + "path" # MIT + ]) + self.scan("modules", [".js", ".jsm"]) self.scan("examples", [".js", ".css", ".html", ".md"]) - self.scan("bin", [".bat", ".ps1"]) + self.scan("bin", [".bat", ".ps1", ".js"]) for fn in [os.path.join("bin", "activate"), os.path.join("bin", "cfx"), os.path.join("bin", "integration-scripts", "buildbot-run-cfx-helper"), os.path.join("bin", "integration-scripts", "integration-check"), ]: self.scan_file(from_sdk_top(fn)) - self.scan("doc", [".js", ".css", ".md"], skipdirs=["syntaxhighlighter"]) if self.missing: print diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py old mode 100644 new mode 100755 diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_rdf.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_rdf.py index b4e389391a9c..5d9254e4e06f 100644 --- a/addon-sdk/source/python-lib/cuddlefish/tests/test_rdf.py +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_rdf.py @@ -6,7 +6,7 @@ import unittest import xml.dom.minidom import os.path -from cuddlefish import rdf, packaging, property_parser +from cuddlefish import rdf, packaging parent = os.path.dirname test_dir = parent(os.path.abspath(__file__)) @@ -49,52 +49,6 @@ class RDFTests(unittest.TestCase): self.failUnlessEqual(m.get('em:name'), 'a long ' + n) self.failUnlessIn('a long ' + n + '', str(m), n) - def testLocalization(self): - # addon_title -> - # addon_author -> - # addon_description -> - # addon_homepageURL -> - localizable_in = ["title", "author", "description", "homepage"] - localized_out = ["name", "creator", "description", "homepageURL"] - - basedir = os.path.join(test_dir, "bug-661083-files/packages") - for n in ["noLocalization", "twoLanguages"]: - harness_options = { "locale" : {} } - pkgdir = os.path.join(basedir, n) - localedir = os.path.join(pkgdir, "locale") - files = os.listdir(localedir) - - for file in files: - filepath = os.path.join(localedir, file) - if os.path.isfile(filepath) and file.endswith(".properties"): - language = file[:-len(".properties")] - try: - parsed_file = property_parser.parse_file(filepath) - except property_parser.MalformedLocaleFileError, msg: - self.fail(msg) - - harness_options["locale"][language] = parsed_file - - cfg = packaging.get_config_in_dir(pkgdir) - m = rdf.gen_manifest(template_dir, cfg, 'JID', harness_options) - - if n == "noLocalization": - self.failIf("" in str(m)) - continue - - for lang in harness_options["locale"]: - rdfstr = str(m) - node = "" + lang + "" - self.failUnlessIn(node, rdfstr, n) - - for value_in in localizable_in: - key_in = "extensions." + m.get('em:id') + "." + value_in - tag_out = localized_out[localizable_in.index(value_in)] - if key_in in harness_options["locale"][lang]: - # E.g. "author-en-US" - node = "" + value_in + "-" + lang \ - + "" - self.failUnlessIn(node , rdfstr, n) if __name__ == '__main__': unittest.main() diff --git a/addon-sdk/source/test/addons/addon-manager/lib/main.js b/addon-sdk/source/test/addons/addon-manager/lib/main.js new file mode 100644 index 000000000000..043424f5935a --- /dev/null +++ b/addon-sdk/source/test/addons/addon-manager/lib/main.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.exports = require("./test-main.js"); + +require('sdk/test/runner').runTestsFromModule(module); diff --git a/addon-sdk/source/test/addons/addon-manager/main.js b/addon-sdk/source/test/addons/addon-manager/lib/test-main.js similarity index 86% rename from addon-sdk/source/test/addons/addon-manager/main.js rename to addon-sdk/source/test/addons/addon-manager/lib/test-main.js index fb5e99053c03..7204c4a40f1e 100644 --- a/addon-sdk/source/test/addons/addon-manager/main.js +++ b/addon-sdk/source/test/addons/addon-manager/lib/test-main.js @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -'use strict'; +"use strict"; const { id } = require("sdk/self"); const { getAddonByID } = require("sdk/addon/manager"); @@ -10,5 +10,3 @@ exports["test getAddonByID"] = function*(assert) { let addon = yield getAddonByID(id); assert.equal(addon.id, id, "getAddonByID works"); } - -require('sdk/test/runner').runTestsFromModule(module); diff --git a/addon-sdk/source/test/addons/addon-manager/package.json b/addon-sdk/source/test/addons/addon-manager/package.json index 9a84bc84d521..2ed7484988ad 100644 --- a/addon-sdk/source/test/addons/addon-manager/package.json +++ b/addon-sdk/source/test/addons/addon-manager/package.json @@ -1,3 +1,7 @@ { - "id": "test-addon-manager" + "id": "test-addon-manager@jetpack", + "main": "./lib/main.js", + "name": "test-addon-manager", + "version": "0.0.1", + "author": "Erik Vold" } diff --git a/addon-sdk/source/test/addons/author-email/package.json b/addon-sdk/source/test/addons/author-email/package.json index 3f52077f115f..2654ec431a80 100644 --- a/addon-sdk/source/test/addons/author-email/package.json +++ b/addon-sdk/source/test/addons/author-email/package.json @@ -1,4 +1,6 @@ { "id": "test-addon-author-email@jetpack", - "author": "test " + "author": "test ", + "version": "0.0.1", + "main": "./main.js" } diff --git a/addon-sdk/source/test/addons/child_process/package.json b/addon-sdk/source/test/addons/child_process/package.json index bd3c9ad0778a..3b882d0c4578 100644 --- a/addon-sdk/source/test/addons/child_process/package.json +++ b/addon-sdk/source/test/addons/child_process/package.json @@ -1,4 +1,5 @@ { - "id": "test-child-process", - "main": "index.js" + "id": "test-child-process@jetpack", + "main": "./index.js", + "version": "0.0.1" } diff --git a/addon-sdk/source/test/addons/chrome/chrome/skin/style.css b/addon-sdk/source/test/addons/chrome/chrome/skin/style.css index 33335159487d..22abf3596337 100644 --- a/addon-sdk/source/test/addons/chrome/chrome/skin/style.css +++ b/addon-sdk/source/test/addons/chrome/chrome/skin/style.css @@ -1 +1,4 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ test{} diff --git a/addon-sdk/source/test/addons/chrome/main.js b/addon-sdk/source/test/addons/chrome/main.js index c9faf1dee95b..62e2d691056b 100644 --- a/addon-sdk/source/test/addons/chrome/main.js +++ b/addon-sdk/source/test/addons/chrome/main.js @@ -9,7 +9,6 @@ const { WindowTracker } = require('sdk/deprecated/window-utils'); const { close, open } = require('sdk/window/helpers'); const { data } = require('sdk/self'); const { Panel } = require('sdk/panel'); -const { setTimeout } = require("sdk/timers") const XUL_URL = 'chrome://test/content/new-window.xul' @@ -23,7 +22,7 @@ exports.testChromeSkin = function(assert, done) { url: skinURL, overrideMimeType: 'text/plain', onComplete: function (response) { - assert.equal(response.text.trim(), 'test{}', 'chrome.manifest skin folder was registered!'); + assert.ok(/test\{\}\s*$/.test(response.text), 'chrome.manifest skin folder was registered!'); done(); } }).get(); @@ -78,13 +77,9 @@ exports.testChromeInPanel = function(assert, done) { assert.pass('panel shown'); panel.port.once('echo', _ => { assert.pass('got echo'); - panel.once('hide', _ => { - assert.pass('panel hidden'); - panel.destroy(); - assert.pass('panel is destroyed'); - done(); - }); - setTimeout(() => panel.hide()); + panel.destroy(); + assert.pass('panel is destroyed'); + done(); }); panel.port.emit('echo'); }); diff --git a/addon-sdk/source/test/addons/chrome/package.json b/addon-sdk/source/test/addons/chrome/package.json index b347cf87271e..82c6db899715 100644 --- a/addon-sdk/source/test/addons/chrome/package.json +++ b/addon-sdk/source/test/addons/chrome/package.json @@ -1,3 +1,5 @@ { - "id": "test-chrome" + "id": "test-chrome@jetpack", + "main": "./main.js", + "version": "0.0.1" } diff --git a/addon-sdk/source/test/addons/content-permissions/package.json b/addon-sdk/source/test/addons/content-permissions/package.json index 00e1a02fdb5f..6e75d20445a0 100644 --- a/addon-sdk/source/test/addons/content-permissions/package.json +++ b/addon-sdk/source/test/addons/content-permissions/package.json @@ -1,6 +1,8 @@ { - "id": "content-permissions", + "id": "content-permissions@jetpack", "permissions": { "cross-domain-content": ["http://localhost:8099"] - } + }, + "main": "./main.js", + "version": "0.0.1" } diff --git a/addon-sdk/source/test/addons/contributors/package.json b/addon-sdk/source/test/addons/contributors/package.json index 6792a8dd69da..b6f1798d3ae3 100644 --- a/addon-sdk/source/test/addons/contributors/package.json +++ b/addon-sdk/source/test/addons/contributors/package.json @@ -1,4 +1,6 @@ { - "id": "test-contributors", - "contributors": [ "A", "B" ] + "id": "test-contributors@jetpack", + "contributors": [ "A", "B" ], + "main": "./main.js", + "version": "0.0.1" } diff --git a/addon-sdk/source/test/addons/curly-id/package.json b/addon-sdk/source/test/addons/curly-id/package.json index 8b387d633ecd..213844662ff8 100644 --- a/addon-sdk/source/test/addons/curly-id/package.json +++ b/addon-sdk/source/test/addons/curly-id/package.json @@ -2,11 +2,12 @@ "id": "{34a1eae1-c20a-464f-9b0e-000000000000}", "fullName": "curly ID test", "author": "Tomislav Jovanovic", - "preferences": [{ "name": "test13", "type": "integer", "title": "test13", "value": 26 - }] + }], + "main": "./lib/main.js", + "version": "0.0.1" } diff --git a/addon-sdk/source/test/addons/developers/package.json b/addon-sdk/source/test/addons/developers/package.json index b5a499b688ee..1d2a189ecb42 100644 --- a/addon-sdk/source/test/addons/developers/package.json +++ b/addon-sdk/source/test/addons/developers/package.json @@ -2,5 +2,7 @@ "id": "test-developers@jetpack", "title": "Test developers package key", "author": "Erik Vold", - "developers": [ "A", "B" ] + "developers": [ "A", "B" ], + "main": "./main.js", + "version": "0.0.1" } diff --git a/addon-sdk/source/test/addons/e10s-content/data/test-contentScriptFile.js b/addon-sdk/source/test/addons/e10s-content/data/test-contentScriptFile.js new file mode 100644 index 000000000000..7dc0e3f24257 --- /dev/null +++ b/addon-sdk/source/test/addons/e10s-content/data/test-contentScriptFile.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +self.postMessage("msg from contentScriptFile"); diff --git a/addon-sdk/source/test/fixtures/test-page-worker.html b/addon-sdk/source/test/addons/e10s-content/data/test-page-worker.html similarity index 100% rename from addon-sdk/source/test/fixtures/test-page-worker.html rename to addon-sdk/source/test/addons/e10s-content/data/test-page-worker.html diff --git a/addon-sdk/source/test/fixtures/test-page-worker.js b/addon-sdk/source/test/addons/e10s-content/data/test-page-worker.js similarity index 100% rename from addon-sdk/source/test/fixtures/test-page-worker.js rename to addon-sdk/source/test/addons/e10s-content/data/test-page-worker.js diff --git a/addon-sdk/source/test/addons/e10s-content/data/test.html b/addon-sdk/source/test/addons/e10s-content/data/test.html new file mode 100644 index 000000000000..181e85f9b1d9 --- /dev/null +++ b/addon-sdk/source/test/addons/e10s-content/data/test.html @@ -0,0 +1,13 @@ + + + + + + foo + + +

bar

+ + diff --git a/addon-sdk/source/test/addons/e10s-content/lib/fixtures.js b/addon-sdk/source/test/addons/e10s-content/lib/fixtures.js new file mode 100644 index 000000000000..d3bd49300d76 --- /dev/null +++ b/addon-sdk/source/test/addons/e10s-content/lib/fixtures.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { data } = require('sdk/self'); + +exports.url = data.url; diff --git a/addon-sdk/source/test/addons/e10s-content/lib/httpd.js b/addon-sdk/source/test/addons/e10s-content/lib/httpd.js new file mode 100644 index 000000000000..1bf539712ca1 --- /dev/null +++ b/addon-sdk/source/test/addons/e10s-content/lib/httpd.js @@ -0,0 +1,5212 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* +* An implementation of an HTTP server both as a loadable script and as an XPCOM +* component. See the accompanying README file for user documentation on +* httpd.js. +*/ + +module.metadata = { + "stability": "experimental" +}; + +const { components, CC, Cc, Ci, Cr, Cu } = require("chrome"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + + +const PR_UINT32_MAX = Math.pow(2, 32) - 1; + +/** True if debugging output is enabled, false otherwise. */ +var DEBUG = false; // non-const *only* so tweakable in server tests + +/** True if debugging output should be timestamped. */ +var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests + +var gGlobalObject = Cc["@mozilla.org/systemprincipal;1"].createInstance(); + +/** +* Asserts that the given condition holds. If it doesn't, the given message is +* dumped, a stack trace is printed, and an exception is thrown to attempt to +* stop execution (which unfortunately must rely upon the exception not being +* accidentally swallowed by the code that uses it). +*/ +function NS_ASSERT(cond, msg) +{ + if (DEBUG && !cond) + { + dumpn("###!!!"); + dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!")); + dumpn("###!!! Stack follows:"); + + var stack = new Error().stack.split(/\n/); + dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n")); + + throw Cr.NS_ERROR_ABORT; + } +} + +/** Constructs an HTTP error object. */ +function HttpError(code, description) +{ + this.code = code; + this.description = description; +} +HttpError.prototype = +{ + toString: function() + { + return this.code + " " + this.description; + } +}; + +/** +* Errors thrown to trigger specific HTTP server responses. +*/ +const HTTP_400 = new HttpError(400, "Bad Request"); +const HTTP_401 = new HttpError(401, "Unauthorized"); +const HTTP_402 = new HttpError(402, "Payment Required"); +const HTTP_403 = new HttpError(403, "Forbidden"); +const HTTP_404 = new HttpError(404, "Not Found"); +const HTTP_405 = new HttpError(405, "Method Not Allowed"); +const HTTP_406 = new HttpError(406, "Not Acceptable"); +const HTTP_407 = new HttpError(407, "Proxy Authentication Required"); +const HTTP_408 = new HttpError(408, "Request Timeout"); +const HTTP_409 = new HttpError(409, "Conflict"); +const HTTP_410 = new HttpError(410, "Gone"); +const HTTP_411 = new HttpError(411, "Length Required"); +const HTTP_412 = new HttpError(412, "Precondition Failed"); +const HTTP_413 = new HttpError(413, "Request Entity Too Large"); +const HTTP_414 = new HttpError(414, "Request-URI Too Long"); +const HTTP_415 = new HttpError(415, "Unsupported Media Type"); +const HTTP_417 = new HttpError(417, "Expectation Failed"); + +const HTTP_500 = new HttpError(500, "Internal Server Error"); +const HTTP_501 = new HttpError(501, "Not Implemented"); +const HTTP_502 = new HttpError(502, "Bad Gateway"); +const HTTP_503 = new HttpError(503, "Service Unavailable"); +const HTTP_504 = new HttpError(504, "Gateway Timeout"); +const HTTP_505 = new HttpError(505, "HTTP Version Not Supported"); + +/** Creates a hash with fields corresponding to the values in arr. */ +function array2obj(arr) +{ + var obj = {}; + for (var i = 0; i < arr.length; i++) + obj[arr[i]] = arr[i]; + return obj; +} + +/** Returns an array of the integers x through y, inclusive. */ +function range(x, y) +{ + var arr = []; + for (var i = x; i <= y; i++) + arr.push(i); + return arr; +} + +/** An object (hash) whose fields are the numbers of all HTTP error codes. */ +const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505))); + + +/** +* The character used to distinguish hidden files from non-hidden files, a la +* the leading dot in Apache. Since that mechanism also hides files from +* easy display in LXR, ls output, etc. however, we choose instead to use a +* suffix character. If a requested file ends with it, we append another +* when getting the file on the server. If it doesn't, we just look up that +* file. Therefore, any file whose name ends with exactly one of the character +* is "hidden" and available for use by the server. +*/ +const HIDDEN_CHAR = "^"; + +/** +* The file name suffix indicating the file containing overridden headers for +* a requested file. +*/ +const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR; + +/** Type used to denote SJS scripts for CGI-like functionality. */ +const SJS_TYPE = "sjs"; + +/** Base for relative timestamps produced by dumpn(). */ +var firstStamp = 0; + +/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */ +function dumpn(str) +{ + if (DEBUG) + { + var prefix = "HTTPD-INFO | "; + if (DEBUG_TIMESTAMP) + { + if (firstStamp === 0) + firstStamp = Date.now(); + + var elapsed = Date.now() - firstStamp; // milliseconds + var min = Math.floor(elapsed / 60000); + var sec = (elapsed % 60000) / 1000; + + if (sec < 10) + prefix += min + ":0" + sec.toFixed(3) + " | "; + else + prefix += min + ":" + sec.toFixed(3) + " | "; + } + + dump(prefix + str + "\n"); + } +} + +/** Dumps the current JS stack if DEBUG. */ +function dumpStack() +{ + // peel off the frames for dumpStack() and Error() + var stack = new Error().stack.split(/\n/).slice(2); + stack.forEach(dumpn); +} + + +/** The XPCOM thread manager. */ +var gThreadManager = null; + +/** The XPCOM prefs service. */ +var gRootPrefBranch = null; +function getRootPrefBranch() +{ + if (!gRootPrefBranch) + { + gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + } + return gRootPrefBranch; +} + +/** +* JavaScript constructors for commonly-used classes; precreating these is a +* speedup over doing the same from base principles. See the docs at +* http://developer.mozilla.org/en/docs/components.Constructor for details. +*/ +const ServerSocket = CC("@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init"); +const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init"); +const Pipe = CC("@mozilla.org/pipe;1", + "nsIPipe", + "init"); +const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", + "init"); +const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1", + "nsIConverterInputStream", + "init"); +const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1", + "nsIWritablePropertyBag2"); +const SupportsString = CC("@mozilla.org/supports-string;1", + "nsISupportsString"); + +/* These two are non-const only so a test can overwrite them. */ +var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); +var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream"); + +/** +* Returns the RFC 822/1123 representation of a date. +* +* @param date : Number +* the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT +* @returns string +* the representation of the given date +*/ +function toDateString(date) +{ + // + // rfc1123-date = wkday "," SP date1 SP time SP "GMT" + // date1 = 2DIGIT SP month SP 4DIGIT + // ; day month year (e.g., 02 Jun 1982) + // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT + // ; 00:00:00 - 23:59:59 + // wkday = "Mon" | "Tue" | "Wed" + // | "Thu" | "Fri" | "Sat" | "Sun" + // month = "Jan" | "Feb" | "Mar" | "Apr" + // | "May" | "Jun" | "Jul" | "Aug" + // | "Sep" | "Oct" | "Nov" | "Dec" + // + + const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + /** +* Processes a date and returns the encoded UTC time as a string according to +* the format specified in RFC 2616. +* +* @param date : Date +* the date to process +* @returns string +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" +*/ + function toTime(date) + { + var hrs = date.getUTCHours(); + var rv = (hrs < 10) ? "0" + hrs : hrs; + + var mins = date.getUTCMinutes(); + rv += ":"; + rv += (mins < 10) ? "0" + mins : mins; + + var secs = date.getUTCSeconds(); + rv += ":"; + rv += (secs < 10) ? "0" + secs : secs; + + return rv; + } + + /** +* Processes a date and returns the encoded UTC date as a string according to +* the date1 format specified in RFC 2616. +* +* @param date : Date +* the date to process +* @returns string +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" +*/ + function toDate1(date) + { + var day = date.getUTCDate(); + var month = date.getUTCMonth(); + var year = date.getUTCFullYear(); + + var rv = (day < 10) ? "0" + day : day; + rv += " " + monthStrings[month]; + rv += " " + year; + + return rv; + } + + date = new Date(date); + + const fmtString = "%wkday%, %date1% %time% GMT"; + var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]); + rv = rv.replace("%time%", toTime(date)); + return rv.replace("%date1%", toDate1(date)); +} + +/** +* Prints out a human-readable representation of the object o and its fields, +* omitting those whose names begin with "_" if showMembers != true (to ignore +* "private" properties exposed via getters/setters). +*/ +function printObj(o, showMembers) +{ + var s = "******************************\n"; + s += "o = {\n"; + for (var i in o) + { + if (typeof(i) != "string" || + (showMembers || (i.length > 0 && i[0] != "_"))) + s+= " " + i + ": " + o[i] + ",\n"; + } + s += " };\n"; + s += "******************************"; + dumpn(s); +} + +/** +* Instantiates a new HTTP server. +*/ +function nsHttpServer() +{ + if (!gThreadManager) + gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + + /** The port on which this server listens. */ + this._port = undefined; + + /** The socket associated with this. */ + this._socket = null; + + /** The handler used to process requests to this server. */ + this._handler = new ServerHandler(this); + + /** Naming information for this server. */ + this._identity = new ServerIdentity(); + + /** +* Indicates when the server is to be shut down at the end of the request. +*/ + this._doQuit = false; + + /** +* True if the socket in this is closed (and closure notifications have been +* sent and processed if the socket was ever opened), false otherwise. +*/ + this._socketClosed = true; + + /** +* Used for tracking existing connections and ensuring that all connections +* are properly cleaned up before server shutdown; increases by 1 for every +* new incoming connection. +*/ + this._connectionGen = 0; + + /** +* Hash of all open connections, indexed by connection number at time of +* creation. +*/ + this._connections = {}; +} +nsHttpServer.prototype = +{ + classID: components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"), + + // NSISERVERSOCKETLISTENER + + /** +* Processes an incoming request coming in on the given socket and contained +* in the given transport. +* +* @param socket : nsIServerSocket +* the socket through which the request was served +* @param trans : nsISocketTransport +* the transport for the request/response +* @see nsIServerSocketListener.onSocketAccepted +*/ + onSocketAccepted: function(socket, trans) + { + dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")"); + + dumpn(">>> new connection on " + trans.host + ":" + trans.port); + + const SEGMENT_SIZE = 8192; + const SEGMENT_COUNT = 1024; + try + { + var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT) + .QueryInterface(Ci.nsIAsyncInputStream); + var output = trans.openOutputStream(0, 0, 0); + } + catch (e) + { + dumpn("*** error opening transport streams: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + var connectionNumber = ++this._connectionGen; + + try + { + var conn = new Connection(input, output, this, socket.port, trans.port, + connectionNumber); + var reader = new RequestReader(conn); + + // XXX add request timeout functionality here! + + // Note: must use main thread here, or we might get a GC that will cause + // threadsafety assertions. We really need to fix XPConnect so that + // you can actually do things in multi-threaded JS. :-( + input.asyncWait(reader, 0, 0, gThreadManager.mainThread); + } + catch (e) + { + // Assume this connection can't be salvaged and bail on it completely; + // don't attempt to close it so that we can assert that any connection + // being closed is in this._connections. + dumpn("*** error in initial request-processing stages: " + e); + trans.close(Cr.NS_BINDING_ABORTED); + return; + } + + this._connections[connectionNumber] = conn; + dumpn("*** starting connection " + connectionNumber); + }, + + /** +* Called when the socket associated with this is closed. +* +* @param socket : nsIServerSocket +* the socket being closed +* @param status : nsresult +* the reason the socket stopped listening (NS_BINDING_ABORTED if the server +* was stopped using nsIHttpServer.stop) +* @see nsIServerSocketListener.onStopListening +*/ + onStopListening: function(socket, status) + { + dumpn(">>> shutting down server on port " + socket.port); + this._socketClosed = true; + if (!this._hasOpenConnections()) + { + dumpn("*** no open connections, notifying async from onStopListening"); + + // Notify asynchronously so that any pending teardown in stop() has a + // chance to run first. + var self = this; + var stopEvent = + { + run: function() + { + dumpn("*** _notifyStopped async callback"); + self._notifyStopped(); + } + }; + gThreadManager.currentThread + .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + + // NSIHTTPSERVER + + // + // see nsIHttpServer.start + // + start: function(port) + { + this._start(port, "localhost") + }, + + _start: function(port, host) + { + if (this._socket) + throw Cr.NS_ERROR_ALREADY_INITIALIZED; + + this._port = port; + this._doQuit = this._socketClosed = false; + + this._host = host; + + // The listen queue needs to be long enough to handle + // network.http.max-persistent-connections-per-server concurrent connections, + // plus a safety margin in case some other process is talking to + // the server as well. + var prefs = getRootPrefBranch(); + var maxConnections; + try { + // Bug 776860: The original pref was removed in favor of this new one: + maxConnections = prefs.getIntPref("network.http.max-persistent-connections-per-server") + 5; + } + catch(e) { + maxConnections = prefs.getIntPref("network.http.max-connections-per-server") + 5; + } + + try + { + var loopback = true; + if (this._host != "127.0.0.1" && this._host != "localhost") { + var loopback = false; + } + + var socket = new ServerSocket(this._port, + loopback, // true = localhost, false = everybody + maxConnections); + dumpn(">>> listening on port " + socket.port + ", " + maxConnections + + " pending connections"); + socket.asyncListen(this); + this._identity._initialize(socket.port, host, true); + this._socket = socket; + } + catch (e) + { + dumpn("!!! could not start server on port " + port + ": " + e); + throw Cr.NS_ERROR_NOT_AVAILABLE; + } + }, + + // + // see nsIHttpServer.stop + // + stop: function(callback) + { + if (!callback) + throw Cr.NS_ERROR_NULL_POINTER; + if (!this._socket) + throw Cr.NS_ERROR_UNEXPECTED; + + this._stopCallback = typeof callback === "function" + ? callback + : function() { callback.onStopped(); }; + + dumpn(">>> stopping listening on port " + this._socket.port); + this._socket.close(); + this._socket = null; + + // We can't have this identity any more, and the port on which we're running + // this server now could be meaningless the next time around. + this._identity._teardown(); + + this._doQuit = false; + + // socket-close notification and pending request completion happen async + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (file && (!file.exists() || file.isDirectory())) + throw Cr.NS_ERROR_INVALID_ARG; + + this._handler.registerFile(path, file); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // XXX true path validation! + if (path.charAt(0) != "/" || + path.charAt(path.length - 1) != "/" || + (directory && + (!directory.exists() || !directory.isDirectory()))) + throw Cr.NS_ERROR_INVALID_ARG; + + // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping + // exists! + + this._handler.registerDirectory(path, directory); + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + this._handler.registerPathHandler(path, handler); + }, + + // + // see nsIHttpServer.registerPrefixHandler + // + registerPrefixHandler: function(prefix, handler) + { + this._handler.registerPrefixHandler(prefix, handler); + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(code, handler) + { + this._handler.registerErrorHandler(code, handler); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler: function(handler) + { + this._handler.setIndexHandler(handler); + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType: function(ext, type) + { + this._handler.registerContentType(ext, type); + }, + + // + // see nsIHttpServer.serverIdentity + // + get identity() + { + return this._identity; + }, + + // + // see nsIHttpServer.getState + // + getState: function(path, k) + { + return this._handler._getState(path, k); + }, + + // + // see nsIHttpServer.setState + // + setState: function(path, k, v) + { + return this._handler._setState(path, k, v); + }, + + // + // see nsIHttpServer.getSharedState + // + getSharedState: function(k) + { + return this._handler._getSharedState(k); + }, + + // + // see nsIHttpServer.setSharedState + // + setSharedState: function(k, v) + { + return this._handler._setSharedState(k, v); + }, + + // + // see nsIHttpServer.getObjectState + // + getObjectState: function(k) + { + return this._handler._getObjectState(k); + }, + + // + // see nsIHttpServer.setObjectState + // + setObjectState: function(k, v) + { + return this._handler._setObjectState(k, v); + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIServerSocketListener) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // NON-XPCOM PUBLIC API + + /** +* Returns true iff this server is not running (and is not in the process of +* serving any requests still to be processed when the server was last +* stopped after being run). +*/ + isStopped: function() + { + return this._socketClosed && !this._hasOpenConnections(); + }, + + // PRIVATE IMPLEMENTATION + + /** True if this server has any open connections to it, false otherwise. */ + _hasOpenConnections: function() + { + // + // If we have any open connections, they're tracked as numeric properties on + // |this._connections|. The non-standard __count__ property could be used + // to check whether there are any properties, but standard-wise, even + // looking forward to ES5, there's no less ugly yet still O(1) way to do + // this. + // + for (var n in this._connections) + return true; + return false; + }, + + /** Calls the server-stopped callback provided when stop() was called. */ + _notifyStopped: function() + { + NS_ASSERT(this._stopCallback !== null, "double-notifying?"); + NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now"); + + // + // NB: We have to grab this now, null out the member, *then* call the + // callback here, or otherwise the callback could (indirectly) futz with + // this._stopCallback by starting and immediately stopping this, at + // which point we'd be nulling out a field we no longer have a right to + // modify. + // + var callback = this._stopCallback; + this._stopCallback = null; + try + { + callback(); + } + catch (e) + { + // not throwing because this is specified as being usually (but not + // always) asynchronous + dump("!!! error running onStopped callback: " + e + "\n"); + } + }, + + /** +* Notifies this server that the given connection has been closed. +* +* @param connection : Connection +* the connection that was closed +*/ + _connectionClosed: function(connection) + { + NS_ASSERT(connection.number in this._connections, + "closing a connection " + this + " that we never added to the " + + "set of open connections?"); + NS_ASSERT(this._connections[connection.number] === connection, + "connection number mismatch? " + + this._connections[connection.number]); + delete this._connections[connection.number]; + + // Fire a pending server-stopped notification if it's our responsibility. + if (!this._hasOpenConnections() && this._socketClosed) + this._notifyStopped(); + }, + + /** +* Requests that the server be shut down when possible. +*/ + _requestQuit: function() + { + dumpn(">>> requesting a quit"); + dumpStack(); + this._doQuit = true; + } +}; + + +// +// RFC 2396 section 3.2.2: +// +// host = hostname | IPv4address +// hostname = *( domainlabel "." ) toplabel [ "." ] +// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum +// toplabel = alpha | alpha *( alphanum | "-" ) alphanum +// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit +// + +const HOST_REGEX = + new RegExp("^(?:" + + // *( domainlabel "." ) + "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" + + // toplabel + "[a-z](?:[a-z0-9-]*[a-z0-9])?" + + "|" + + // IPv4 address + "\\d+\\.\\d+\\.\\d+\\.\\d+" + + ")$", + "i"); + + +/** +* Represents the identity of a server. An identity consists of a set of +* (scheme, host, port) tuples denoted as locations (allowing a single server to +* serve multiple sites or to be used behind both HTTP and HTTPS proxies for any +* host/port). Any incoming request must be to one of these locations, or it +* will be rejected with an HTTP 400 error. One location, denoted as the +* primary location, is the location assigned in contexts where a location +* cannot otherwise be endogenously derived, such as for HTTP/1.0 requests. +* +* A single identity may contain at most one location per unique host/port pair; +* other than that, no restrictions are placed upon what locations may +* constitute an identity. +*/ +function ServerIdentity() +{ + /** The scheme of the primary location. */ + this._primaryScheme = "http"; + + /** The hostname of the primary location. */ + this._primaryHost = "127.0.0.1" + + /** The port number of the primary location. */ + this._primaryPort = -1; + + /** +* The current port number for the corresponding server, stored so that a new +* primary location can always be set if the current one is removed. +*/ + this._defaultPort = -1; + + /** +* Maps hosts to maps of ports to schemes, e.g. the following would represent +* https://example.com:789/ and http://example.org/: +* +* { +* "xexample.com": { 789: "https" }, +* "xexample.org": { 80: "http" } +* } +* +* Note the "x" prefix on hostnames, which prevents collisions with special +* JS names like "prototype". +*/ + this._locations = { "xlocalhost": {} }; +} +ServerIdentity.prototype = +{ + // NSIHTTPSERVERIDENTITY + + // + // see nsIHttpServerIdentity.primaryScheme + // + get primaryScheme() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryScheme; + }, + + // + // see nsIHttpServerIdentity.primaryHost + // + get primaryHost() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryHost; + }, + + // + // see nsIHttpServerIdentity.primaryPort + // + get primaryPort() + { + if (this._primaryPort === -1) + throw Cr.NS_ERROR_NOT_INITIALIZED; + return this._primaryPort; + }, + + // + // see nsIHttpServerIdentity.add + // + add: function(scheme, host, port) + { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) + this._locations["x" + host] = entry = {}; + + entry[port] = scheme; + }, + + // + // see nsIHttpServerIdentity.remove + // + remove: function(scheme, host, port) + { + this._validate(scheme, host, port); + + var entry = this._locations["x" + host]; + if (!entry) + return false; + + var present = port in entry; + delete entry[port]; + + if (this._primaryScheme == scheme && + this._primaryHost == host && + this._primaryPort == port && + this._defaultPort !== -1) + { + // Always keep at least one identity in existence at any time, unless + // we're in the process of shutting down (the last condition above). + this._primaryPort = -1; + this._initialize(this._defaultPort, host, false); + } + + return present; + }, + + // + // see nsIHttpServerIdentity.has + // + has: function(scheme, host, port) + { + this._validate(scheme, host, port); + + return "x" + host in this._locations && + scheme === this._locations["x" + host][port]; + }, + + // + // see nsIHttpServerIdentity.has + // + getScheme: function(host, port) + { + this._validate("http", host, port); + + var entry = this._locations["x" + host]; + if (!entry) + return ""; + + return entry[port] || ""; + }, + + // + // see nsIHttpServerIdentity.setPrimary + // + setPrimary: function(scheme, host, port) + { + this._validate(scheme, host, port); + + this.add(scheme, host, port); + + this._primaryScheme = scheme; + this._primaryHost = host; + this._primaryPort = port; + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE IMPLEMENTATION + + /** +* Initializes the primary name for the corresponding server, based on the +* provided port number. +*/ + _initialize: function(port, host, addSecondaryDefault) + { + this._host = host; + if (this._primaryPort !== -1) + this.add("http", host, port); + else + this.setPrimary("http", "localhost", port); + this._defaultPort = port; + + // Only add this if we're being called at server startup + if (addSecondaryDefault && host != "127.0.0.1") + this.add("http", "127.0.0.1", port); + }, + + /** +* Called at server shutdown time, unsets the primary location only if it was +* the default-assigned location and removes the default location from the +* set of locations used. +*/ + _teardown: function() + { + if (this._host != "127.0.0.1") { + // Not the default primary location, nothing special to do here + this.remove("http", "127.0.0.1", this._defaultPort); + } + + // This is a *very* tricky bit of reasoning here; make absolutely sure the + // tests for this code pass before you commit changes to it. + if (this._primaryScheme == "http" && + this._primaryHost == this._host && + this._primaryPort == this._defaultPort) + { + // Make sure we don't trigger the readding logic in .remove(), then remove + // the default location. + var port = this._defaultPort; + this._defaultPort = -1; + this.remove("http", this._host, port); + + // Ensure a server start triggers the setPrimary() path in ._initialize() + this._primaryPort = -1; + } + else + { + // No reason not to remove directly as it's not our primary location + this.remove("http", this._host, this._defaultPort); + } + }, + + /** +* Ensures scheme, host, and port are all valid with respect to RFC 2396. +* +* @throws NS_ERROR_ILLEGAL_VALUE +* if any argument doesn't match the corresponding production +*/ + _validate: function(scheme, host, port) + { + if (scheme !== "http" && scheme !== "https") + { + dumpn("*** server only supports http/https schemes: '" + scheme + "'"); + dumpStack(); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + if (!HOST_REGEX.test(host)) + { + dumpn("*** unexpected host: '" + host + "'"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + if (port < 0 || port > 65535) + { + dumpn("*** unexpected port: '" + port + "'"); + throw Cr.NS_ERROR_ILLEGAL_VALUE; + } + } +}; + + +/** +* Represents a connection to the server (and possibly in the future the thread +* on which the connection is processed). +* +* @param input : nsIInputStream +* stream from which incoming data on the connection is read +* @param output : nsIOutputStream +* stream to write data out the connection +* @param server : nsHttpServer +* the server handling the connection +* @param port : int +* the port on which the server is running +* @param outgoingPort : int +* the outgoing port used by this connection +* @param number : uint +* a serial number used to uniquely identify this connection +*/ +function Connection(input, output, server, port, outgoingPort, number) +{ + dumpn("*** opening new connection " + number + " on port " + outgoingPort); + + /** Stream of incoming data. */ + this.input = input; + + /** Stream for outgoing data. */ + this.output = output; + + /** The server associated with this request. */ + this.server = server; + + /** The port on which the server is running. */ + this.port = port; + + /** The outgoing poort used by this connection. */ + this._outgoingPort = outgoingPort; + + /** The serial number of this connection. */ + this.number = number; + + /** +* The request for which a response is being generated, null if the +* incoming request has not been fully received or if it had errors. +*/ + this.request = null; + + /** State variables for debugging. */ + this._closed = this._processed = false; +} +Connection.prototype = +{ + /** Closes this connection's input/output streams. */ + close: function() + { + dumpn("*** closing connection " + this.number + + " on port " + this._outgoingPort); + + this.input.close(); + this.output.close(); + this._closed = true; + + var server = this.server; + server._connectionClosed(this); + + // If an error triggered a server shutdown, act on it now + if (server._doQuit) + server.stop(function() { /* not like we can do anything better */ }); + }, + + /** +* Initiates processing of this connection, using the data in the given +* request. +* +* @param request : Request +* the request which should be processed +*/ + process: function(request) + { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + + this.request = request; + this.server._handler.handleResponse(this); + }, + + /** +* Initiates processing of this connection, generating a response with the +* given HTTP error code. +* +* @param code : uint +* an HTTP code, so in the range [0, 1000) +* @param request : Request +* incomplete data about the incoming request (since there were errors +* during its processing +*/ + processError: function(code, request) + { + NS_ASSERT(!this._closed && !this._processed); + + this._processed = true; + this.request = request; + this.server._handler.handleError(code, this); + }, + + /** Converts this to a string for debugging purposes. */ + toString: function() + { + return ""; + } +}; + + + +/** Returns an array of count bytes from the given input stream. */ +function readBytes(inputStream, count) +{ + return new BinaryInputStream(inputStream).readByteArray(count); +} + + + +/** Request reader processing states; see RequestReader for details. */ +const READER_IN_REQUEST_LINE = 0; +const READER_IN_HEADERS = 1; +const READER_IN_BODY = 2; +const READER_FINISHED = 3; + + +/** +* Reads incoming request data asynchronously, does any necessary preprocessing, +* and forwards it to the request handler. Processing occurs in three states: +* +* READER_IN_REQUEST_LINE Reading the request's status line +* READER_IN_HEADERS Reading headers in the request +* READER_IN_BODY Reading the body of the request +* READER_FINISHED Entire request has been read and processed +* +* During the first two stages, initial metadata about the request is gathered +* into a Request object. Once the status line and headers have been processed, +* we start processing the body of the request into the Request. Finally, when +* the entire body has been read, we create a Response and hand it off to the +* ServerHandler to be given to the appropriate request handler. +* +* @param connection : Connection +* the connection for the request being read +*/ +function RequestReader(connection) +{ + /** Connection metadata for this request. */ + this._connection = connection; + + /** +* A container providing line-by-line access to the raw bytes that make up the +* data which has been read from the connection but has not yet been acted +* upon (by passing it to the request handler or by extracting request +* metadata from it). +*/ + this._data = new LineData(); + + /** +* The amount of data remaining to be read from the body of this request. +* After all headers in the request have been read this is the value in the +* Content-Length header, but as the body is read its value decreases to zero. +*/ + this._contentLength = 0; + + /** The current state of parsing the incoming request. */ + this._state = READER_IN_REQUEST_LINE; + + /** Metadata constructed from the incoming request for the request handler. */ + this._metadata = new Request(connection.port); + + /** +* Used to preserve state if we run out of line data midway through a +* multi-line header. _lastHeaderName stores the name of the header, while +* _lastHeaderValue stores the value we've seen so far for the header. +* +* These fields are always either both undefined or both strings. +*/ + this._lastHeaderName = this._lastHeaderValue = undefined; +} +RequestReader.prototype = +{ + // NSIINPUTSTREAMCALLBACK + + /** +* Called when more data from the incoming request is available. This method +* then reads the available data from input and deals with that data as +* necessary, depending upon the syntax of already-downloaded data. +* +* @param input : nsIAsyncInputStream +* the stream of incoming data from the connection +*/ + onInputStreamReady: function(input) + { + dumpn("*** onInputStreamReady(input=" + input + ") on thread " + + gThreadManager.currentThread + " (main is " + + gThreadManager.mainThread + ")"); + dumpn("*** this._state == " + this._state); + + // Handle cases where we get more data after a request error has been + // discovered but *before* we can close the connection. + var data = this._data; + if (!data) + return; + + try + { + data.appendBytes(readBytes(input, input.available())); + } + catch (e) + { + if (streamClosed(e)) + { + dumpn("*** WARNING: unexpected error when reading from socket; will " + + "be treated as if the input stream had been closed"); + dumpn("*** WARNING: actual error was: " + e); + } + + // We've lost a race -- input has been closed, but we're still expecting + // to read more data. available() will throw in this case, and since + // we're dead in the water now, destroy the connection. + dumpn("*** onInputStreamReady called on a closed input, destroying " + + "connection"); + this._connection.close(); + return; + } + + switch (this._state) + { + default: + NS_ASSERT(false, "invalid state: " + this._state); + break; + + case READER_IN_REQUEST_LINE: + if (!this._processRequestLine()) + break; + /* fall through */ + + case READER_IN_HEADERS: + if (!this._processHeaders()) + break; + /* fall through */ + + case READER_IN_BODY: + this._processBody(); + } + + if (this._state != READER_FINISHED) + input.asyncWait(this, 0, 0, gThreadManager.currentThread); + }, + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIInputStreamCallback) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE API + + /** +* Processes unprocessed, downloaded data as a request line. +* +* @returns boolean +* true iff the request line has been fully processed +*/ + _processRequestLine: function() + { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + // Servers SHOULD ignore any empty line(s) received where a Request-Line + // is expected (section 4.1). + var data = this._data; + var line = {}; + var readSuccess; + while ((readSuccess = data.readLine(line)) && line.value == "") + dumpn("*** ignoring beginning blank line..."); + + // if we don't have a full line, wait until we do + if (!readSuccess) + return false; + + // we have the first non-blank line + try + { + this._parseRequestLine(line.value); + this._state = READER_IN_HEADERS; + return true; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Processes stored data, assuming it is either at the beginning or in +* the middle of processing request headers. +* +* @returns boolean +* true iff header data in the request has been fully processed +*/ + _processHeaders: function() + { + NS_ASSERT(this._state == READER_IN_HEADERS); + + // XXX things to fix here: + // + // - need to support RFC 2047-encoded non-US-ASCII characters + + try + { + var done = this._parseHeaders(); + if (done) + { + var request = this._metadata; + + // XXX this is wrong for requests with transfer-encodings applied to + // them, particularly chunked (which by its nature can have no + // meaningful Content-Length header)! + this._contentLength = request.hasHeader("Content-Length") + ? parseInt(request.getHeader("Content-Length"), 10) + : 0; + dumpn("_processHeaders, Content-length=" + this._contentLength); + + this._state = READER_IN_BODY; + } + return done; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Processes stored data, assuming it is either at the beginning or in +* the middle of processing the request body. +* +* @returns boolean +* true iff the request body has been fully processed +*/ + _processBody: function() + { + NS_ASSERT(this._state == READER_IN_BODY); + + // XXX handle chunked transfer-coding request bodies! + + try + { + if (this._contentLength > 0) + { + var data = this._data.purge(); + var count = Math.min(data.length, this._contentLength); + dumpn("*** loading data=" + data + " len=" + data.length + + " excess=" + (data.length - count)); + + var bos = new BinaryOutputStream(this._metadata._bodyOutputStream); + bos.writeByteArray(data, count); + this._contentLength -= count; + } + + dumpn("*** remaining body data len=" + this._contentLength); + if (this._contentLength == 0) + { + this._validateRequest(); + this._state = READER_FINISHED; + this._handleResponse(); + return true; + } + + return false; + } + catch (e) + { + this._handleError(e); + return false; + } + }, + + /** +* Does various post-header checks on the data in this request. +* +* @throws : HttpError +* if the request was malformed in some way +*/ + _validateRequest: function() + { + NS_ASSERT(this._state == READER_IN_BODY); + + dumpn("*** _validateRequest"); + + var metadata = this._metadata; + var headers = metadata._headers; + + // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header + var identity = this._connection.server.identity; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) + { + if (!headers.hasHeader("Host")) + { + dumpn("*** malformed HTTP/1.1 or greater request with no Host header!"); + throw HTTP_400; + } + + // If the Request-URI wasn't absolute, then we need to determine our host. + // We have to determine what scheme was used to access us based on the + // server identity data at this point, because the request just doesn't + // contain enough data on its own to do this, sadly. + if (!metadata._host) + { + var host, port; + var hostPort = headers.getHeader("Host"); + var colon = hostPort.indexOf(":"); + if (colon < 0) + { + host = hostPort; + port = ""; + } + else + { + host = hostPort.substring(0, colon); + port = hostPort.substring(colon + 1); + } + + // NB: We allow an empty port here because, oddly, a colon may be + // present even without a port number, e.g. "example.com:"; in this + // case the default port applies. + if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) + { + dumpn("*** malformed hostname (" + hostPort + ") in Host " + + "header, 400 time"); + throw HTTP_400; + } + + // If we're not given a port, we're stuck, because we don't know what + // scheme to use to look up the correct port here, in general. Since + // the HTTPS case requires a tunnel/proxy and thus requires that the + // requested URI be absolute (and thus contain the necessary + // information), let's assume HTTP will prevail and use that. + port = +port || 80; + + var scheme = identity.getScheme(host, port); + if (!scheme) + { + dumpn("*** unrecognized hostname (" + hostPort + ") in Host " + + "header, 400 time"); + throw HTTP_400; + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + } + } + else + { + NS_ASSERT(metadata._host === undefined, + "HTTP/1.0 doesn't allow absolute paths in the request line!"); + + metadata._scheme = identity.primaryScheme; + metadata._host = identity.primaryHost; + metadata._port = identity.primaryPort; + } + + NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port), + "must have a location we recognize by now!"); + }, + + /** +* Handles responses in case of error, either in the server or in the request. +* +* @param e +* the specific error encountered, which is an HttpError in the case where +* the request is in some way invalid or cannot be fulfilled; if this isn't +* an HttpError we're going to be paranoid and shut down, because that +* shouldn't happen, ever +*/ + _handleError: function(e) + { + // Don't fall back into normal processing! + this._state = READER_FINISHED; + + var server = this._connection.server; + if (e instanceof HttpError) + { + var code = e.code; + } + else + { + dumpn("!!! UNEXPECTED ERROR: " + e + + (e.lineNumber ? ", line " + e.lineNumber : "")); + + // no idea what happened -- be paranoid and shut down + code = 500; + server._requestQuit(); + } + + // make attempted reuse of data an error + this._data = null; + + this._connection.processError(code, this._metadata); + }, + + /** +* Now that we've read the request line and headers, we can actually hand off +* the request to be handled. +* +* This method is called once per request, after the request line and all +* headers and the body, if any, have been received. +*/ + _handleResponse: function() + { + NS_ASSERT(this._state == READER_FINISHED); + + // We don't need the line-based data any more, so make attempted reuse an + // error. + this._data = null; + + this._connection.process(this._metadata); + }, + + + // PARSING + + /** +* Parses the request line for the HTTP request associated with this. +* +* @param line : string +* the request line +*/ + _parseRequestLine: function(line) + { + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); + + dumpn("*** _parseRequestLine('" + line + "')"); + + var metadata = this._metadata; + + // clients and servers SHOULD accept any amount of SP or HT characters + // between fields, even though only a single SP is required (section 19.3) + var request = line.split(/[ \t]+/); + if (!request || request.length != 3) + throw HTTP_400; + + metadata._method = request[0]; + + // get the HTTP version + var ver = request[2]; + var match = ver.match(/^HTTP\/(\d+\.\d+)$/); + if (!match) + throw HTTP_400; + + // determine HTTP version + try + { + metadata._httpVersion = new nsHttpVersion(match[1]); + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) + throw "unsupported HTTP version"; + } + catch (e) + { + // we support HTTP/1.0 and HTTP/1.1 only + throw HTTP_501; + } + + + var fullPath = request[1]; + var serverIdentity = this._connection.server.identity; + + var scheme, host, port; + + if (fullPath.charAt(0) != "/") + { + // No absolute paths in the request line in HTTP prior to 1.1 + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) + throw HTTP_400; + + try + { + var uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(fullPath, null, null); + fullPath = uri.path; + scheme = uri.scheme; + host = metadata._host = uri.asciiHost; + port = uri.port; + if (port === -1) + { + if (scheme === "http") + port = 80; + else if (scheme === "https") + port = 443; + else + throw HTTP_400; + } + } + catch (e) + { + // If the host is not a valid host on the server, the response MUST be a + // 400 (Bad Request) error message (section 5.2). Alternately, the URI + // is malformed. + throw HTTP_400; + } + + if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/") + throw HTTP_400; + } + + var splitter = fullPath.indexOf("?"); + if (splitter < 0) + { + // _queryString already set in ctor + metadata._path = fullPath; + } + else + { + metadata._path = fullPath.substring(0, splitter); + metadata._queryString = fullPath.substring(splitter + 1); + } + + metadata._scheme = scheme; + metadata._host = host; + metadata._port = port; + }, + + /** +* Parses all available HTTP headers in this until the header-ending CRLFCRLF, +* adding them to the store of headers in the request. +* +* @throws +* HTTP_400 if the headers are malformed +* @returns boolean +* true if all headers have now been processed, false otherwise +*/ + _parseHeaders: function() + { + NS_ASSERT(this._state == READER_IN_HEADERS); + + dumpn("*** _parseHeaders"); + + var data = this._data; + + var headers = this._metadata._headers; + var lastName = this._lastHeaderName; + var lastVal = this._lastHeaderValue; + + var line = {}; + while (true) + { + NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)), + lastName === undefined ? + "lastVal without lastName? lastVal: '" + lastVal + "'" : + "lastName without lastVal? lastName: '" + lastName + "'"); + + if (!data.readLine(line)) + { + // save any data we have from the header we might still be processing + this._lastHeaderName = lastName; + this._lastHeaderValue = lastVal; + return false; + } + + var lineText = line.value; + var firstChar = lineText.charAt(0); + + // blank line means end of headers + if (lineText == "") + { + // we're finished with the previous header + if (lastName) + { + try + { + headers.setHeader(lastName, lastVal, true); + } + catch (e) + { + dumpn("*** e == " + e); + throw HTTP_400; + } + } + else + { + // no headers in request -- valid for HTTP/1.0 requests + } + + // either way, we're done processing headers + this._state = READER_IN_BODY; + return true; + } + else if (firstChar == " " || firstChar == "\t") + { + // multi-line header if we've already seen a header line + if (!lastName) + { + // we don't have a header to continue! + throw HTTP_400; + } + + // append this line's text to the value; starts with SP/HT, so no need + // for separating whitespace + lastVal += lineText; + } + else + { + // we have a new header, so set the old one (if one existed) + if (lastName) + { + try + { + headers.setHeader(lastName, lastVal, true); + } + catch (e) + { + dumpn("*** e == " + e); + throw HTTP_400; + } + } + + var colon = lineText.indexOf(":"); // first colon must be splitter + if (colon < 1) + { + // no colon or missing header field-name + throw HTTP_400; + } + + // set header name, value (to be set in the next loop, usually) + lastName = lineText.substring(0, colon); + lastVal = lineText.substring(colon + 1); + } // empty, continuation, start of header + } // while (true) + } +}; + + +/** The character codes for CR and LF. */ +const CR = 0x0D, LF = 0x0A; + +/** +* Calculates the number of characters before the first CRLF pair in array, or +* -1 if the array contains no CRLF pair. +* +* @param array : Array +* an array of numbers in the range [0, 256), each representing a single +* character; the first CRLF is the lowest index i where +* |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|, +* if such an |i| exists, and -1 otherwise +* @returns int +* the index of the first CRLF if any were present, -1 otherwise +*/ +function findCRLF(array) +{ + for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1)) + { + if (array[i + 1] == LF) + return i; + } + return -1; +} + + +/** +* A container which provides line-by-line access to the arrays of bytes with +* which it is seeded. +*/ +function LineData() +{ + /** An array of queued bytes from which to get line-based characters. */ + this._data = []; +} +LineData.prototype = +{ + /** +* Appends the bytes in the given array to the internal data cache maintained +* by this. +*/ + appendBytes: function(bytes) + { + Array.prototype.push.apply(this._data, bytes); + }, + + /** +* Removes and returns a line of data, delimited by CRLF, from this. +* +* @param out +* an object whose "value" property will be set to the first line of text +* present in this, sans CRLF, if this contains a full CRLF-delimited line +* of text; if this doesn't contain enough data, the value of the property +* is undefined +* @returns boolean +* true if a full line of data could be read from the data in this, false +* otherwise +*/ + readLine: function(out) + { + var data = this._data; + var length = findCRLF(data); + if (length < 0) + return false; + + // + // We have the index of the CR, so remove all the characters, including + // CRLF, from the array with splice, and convert the removed array into the + // corresponding string, from which we then strip the trailing CRLF. + // + // Getting the line in this matter acknowledges that substring is an O(1) + // operation in SpiderMonkey because strings are immutable, whereas two + // splices, both from the beginning of the data, are less likely to be as + // cheap as a single splice plus two extra character conversions. + // + var line = String.fromCharCode.apply(null, data.splice(0, length + 2)); + out.value = line.substring(0, length); + + return true; + }, + + /** +* Removes the bytes currently within this and returns them in an array. +* +* @returns Array +* the bytes within this when this method is called +*/ + purge: function() + { + var data = this._data; + this._data = []; + return data; + } +}; + + + +/** +* Creates a request-handling function for an nsIHttpRequestHandler object. +*/ +function createHandlerFunc(handler) +{ + return function(metadata, response) { handler.handle(metadata, response); }; +} + + +/** +* The default handler for directories; writes an HTML response containing a +* slightly-formatted directory listing. +*/ +function defaultIndexHandler(metadata, response) +{ + response.setHeader("Content-Type", "text/html", false); + + var path = htmlEscape(decodeURI(metadata.path)); + + // + // Just do a very basic bit of directory listings -- no need for too much + // fanciness, especially since we don't have a style sheet in which we can + // stick rules (don't want to pollute the default path-space). + // + + var body = '\ +\ +' + path + '\ +\ +\ +

' + path + '

\ +
    '; + + var directory = metadata.getProperty("directory").QueryInterface(Ci.nsILocalFile); + NS_ASSERT(directory && directory.isDirectory()); + + var fileList = []; + var files = directory.directoryEntries; + while (files.hasMoreElements()) + { + var f = files.getNext().QueryInterface(Ci.nsIFile); + var name = f.leafName; + if (!f.isHidden() && + (name.charAt(name.length - 1) != HIDDEN_CHAR || + name.charAt(name.length - 2) == HIDDEN_CHAR)) + fileList.push(f); + } + + fileList.sort(fileSort); + + for (var i = 0; i < fileList.length; i++) + { + var file = fileList[i]; + try + { + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) + name = name.substring(0, name.length - 1); + var sep = file.isDirectory() ? "/" : ""; + + // Note: using " to delimit the attribute here because encodeURIComponent + // passes through '. + var item = '
  1. ' + + htmlEscape(name) + sep + + '
  2. '; + + body += item; + } + catch (e) { /* some file system error, ignore the file */ } + } + + body += '
\ +\ +'; + + response.bodyOutputStream.write(body, body.length); +} + +/** +* Sorts a and b (nsIFile objects) into an aesthetically pleasing order. +*/ +function fileSort(a, b) +{ + var dira = a.isDirectory(), dirb = b.isDirectory(); + + if (dira && !dirb) + return -1; + if (dirb && !dira) + return 1; + + var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase(); + return nameb > namea ? -1 : 1; +} + + +/** +* Converts an externally-provided path into an internal path for use in +* determining file mappings. +* +* @param path +* the path to convert +* @param encoded +* true if the given path should be passed through decodeURI prior to +* conversion +* @throws URIError +* if path is incorrectly encoded +*/ +function toInternalPath(path, encoded) +{ + if (encoded) + path = decodeURI(path); + + var comps = path.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) + { + var comp = comps[i]; + if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) + comps[i] = comp + HIDDEN_CHAR; + } + return comps.join("/"); +} + + +/** +* Adds custom-specified headers for the given file to the given response, if +* any such headers are specified. +* +* @param file +* the file on the disk which is to be written +* @param metadata +* metadata about the incoming request +* @param response +* the Response to which any specified headers/data should be written +* @throws HTTP_500 +* if an error occurred while processing custom-specified headers +*/ +function maybeAddHeaders(file, metadata, response) +{ + var name = file.leafName; + if (name.charAt(name.length - 1) == HIDDEN_CHAR) + name = name.substring(0, name.length - 1); + + var headerFile = file.parent; + headerFile.append(name + HEADERS_SUFFIX); + + if (!headerFile.exists()) + return; + + const PR_RDONLY = 0x01; + var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + try + { + var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + + var line = {value: ""}; + var more = lis.readLine(line); + + if (!more && line.value == "") + return; + + + // request line + + var status = line.value; + if (status.indexOf("HTTP ") == 0) + { + status = status.substring(5); + var space = status.indexOf(" "); + var code, description; + if (space < 0) + { + code = status; + description = ""; + } + else + { + code = status.substring(0, space); + description = status.substring(space + 1, status.length); + } + + response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description); + + line.value = ""; + more = lis.readLine(line); + } + + // headers + while (more || line.value != "") + { + var header = line.value; + var colon = header.indexOf(":"); + + response.setHeader(header.substring(0, colon), + header.substring(colon + 1, header.length), + false); // allow overriding server-set headers + + line.value = ""; + more = lis.readLine(line); + } + } + catch (e) + { + dumpn("WARNING: error in headers for " + metadata.path + ": " + e); + throw HTTP_500; + } + finally + { + fis.close(); + } +} + + +/** +* An object which handles requests for a server, executing default and +* overridden behaviors as instructed by the code which uses and manipulates it. +* Default behavior includes the paths / and /trace (diagnostics), with some +* support for HTTP error pages for various codes and fallback to HTTP 500 if +* those codes fail for any reason. +* +* @param server : nsHttpServer +* the server in which this handler is being used +*/ +function ServerHandler(server) +{ + // FIELDS + + /** +* The nsHttpServer instance associated with this handler. +*/ + this._server = server; + + /** +* A FileMap object containing the set of path->nsILocalFile mappings for +* all directory mappings set in the server (e.g., "/" for /var/www/html/, +* "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2). +* +* Note carefully: the leading and trailing "/" in each path (not file) are +* removed before insertion to simplify the code which uses this. You have +* been warned! +*/ + this._pathDirectoryMap = new FileMap(); + + /** +* Custom request handlers for the server in which this resides. Path-handler +* pairs are stored as property-value pairs in this property. +* +* @see ServerHandler.prototype._defaultPaths +*/ + this._overridePaths = {}; + + /** +* Custom request handlers for the server in which this resides. Prefix-handler +* pairs are stored as property-value pairs in this property. +*/ + this._overridePrefixes = {}; + + /** +* Custom request handlers for the error handlers in the server in which this +* resides. Path-handler pairs are stored as property-value pairs in this +* property. +* +* @see ServerHandler.prototype._defaultErrors +*/ + this._overrideErrors = {}; + + /** +* Maps file extensions to their MIME types in the server, overriding any +* mapping that might or might not exist in the MIME service. +*/ + this._mimeMappings = {}; + + /** +* The default handler for requests for directories, used to serve directories +* when no index file is present. +*/ + this._indexHandler = defaultIndexHandler; + + /** Per-path state storage for the server. */ + this._state = {}; + + /** Entire-server state storage. */ + this._sharedState = {}; + + /** Entire-server state storage for nsISupports values. */ + this._objectState = {}; +} +ServerHandler.prototype = +{ + // PUBLIC API + + /** +* Handles a request to this server, responding to the request appropriately +* and initiating server shutdown if necessary. +* +* This method never throws an exception. +* +* @param connection : Connection +* the connection for this request +*/ + handleResponse: function(connection) + { + var request = connection.request; + var response = new Response(connection); + + var path = request.path; + dumpn("*** path == " + path); + + try + { + try + { + if (path in this._overridePaths) + { + // explicit paths first, then files based on existing directory mappings, + // then (if the file doesn't exist) built-in server default paths + dumpn("calling override for " + path); + this._overridePaths[path](request, response); + } + else + { + let longestPrefix = ""; + for (let prefix in this._overridePrefixes) + { + if (prefix.length > longestPrefix.length && path.startsWith(prefix)) + { + longestPrefix = prefix; + } + } + if (longestPrefix.length > 0) + { + dumpn("calling prefix override for " + longestPrefix); + this._overridePrefixes[longestPrefix](request, response); + } + else + { + this._handleDefault(request, response); + } + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + if (!(e instanceof HttpError)) + { + dumpn("*** unexpected error: e == " + e); + throw HTTP_500; + } + if (e.code !== 404) + throw e; + + dumpn("*** default: " + (path in this._defaultPaths)); + + response = new Response(connection); + if (path in this._defaultPaths) + this._defaultPaths[path](request, response); + else + throw HTTP_404; + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + var errorCode = "internal"; + + try + { + if (!(e instanceof HttpError)) + throw e; + + errorCode = e.code; + dumpn("*** errorCode == " + errorCode); + + response = new Response(connection); + if (e.customErrorHandling) + e.customErrorHandling(response); + this._handleError(errorCode, request, response); + return; + } + catch (e2) + { + dumpn("*** error handling " + errorCode + " error: " + + "e2 == " + e2 + ", shutting down server"); + + connection.server._requestQuit(); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // + // see nsIHttpServer.registerFile + // + registerFile: function(path, file) + { + if (!file) + { + dumpn("*** unregistering '" + path + "' mapping"); + delete this._overridePaths[path]; + return; + } + + dumpn("*** registering '" + path + "' as mapping to " + file.path); + file = file.clone(); + + var self = this; + this._overridePaths[path] = + function(request, response) + { + if (!file.exists()) + throw HTTP_404; + + response.setStatusLine(request.httpVersion, 200, "OK"); + self._writeFileResponse(request, file, response, 0, file.fileSize); + }; + }, + + // + // see nsIHttpServer.registerPathHandler + // + registerPathHandler: function(path, handler) + { + // XXX true path validation! + if (path.charAt(0) != "/") + throw Cr.NS_ERROR_INVALID_ARG; + + this._handlerToField(handler, this._overridePaths, path); + }, + + // + // see nsIHttpServer.registerPrefixHandler + // + registerPrefixHandler: function(prefix, handler) + { + // XXX true prefix validation! + if (!(prefix.startsWith("/") && prefix.endsWith("/"))) + throw Cr.NS_ERROR_INVALID_ARG; + + this._handlerToField(handler, this._overridePrefixes, prefix); + }, + + // + // see nsIHttpServer.registerDirectory + // + registerDirectory: function(path, directory) + { + // strip off leading and trailing '/' so that we can use lastIndexOf when + // determining exactly how a path maps onto a mapped directory -- + // conditional is required here to deal with "/".substring(1, 0) being + // converted to "/".substring(0, 1) per the JS specification + var key = path.length == 1 ? "" : path.substring(1, path.length - 1); + + // the path-to-directory mapping code requires that the first character not + // be "/", or it will go into an infinite loop + if (key.charAt(0) == "/") + throw Cr.NS_ERROR_INVALID_ARG; + + key = toInternalPath(key, false); + + if (directory) + { + dumpn("*** mapping '" + path + "' to the location " + directory.path); + this._pathDirectoryMap.put(key, directory); + } + else + { + dumpn("*** removing mapping for '" + path + "'"); + this._pathDirectoryMap.put(key, null); + } + }, + + // + // see nsIHttpServer.registerErrorHandler + // + registerErrorHandler: function(err, handler) + { + if (!(err in HTTP_ERROR_CODES)) + dumpn("*** WARNING: registering non-HTTP/1.1 error code " + + "(" + err + ") handler -- was this intentional?"); + + this._handlerToField(handler, this._overrideErrors, err); + }, + + // + // see nsIHttpServer.setIndexHandler + // + setIndexHandler: function(handler) + { + if (!handler) + handler = defaultIndexHandler; + else if (typeof(handler) != "function") + handler = createHandlerFunc(handler); + + this._indexHandler = handler; + }, + + // + // see nsIHttpServer.registerContentType + // + registerContentType: function(ext, type) + { + if (!type) + delete this._mimeMappings[ext]; + else + this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type); + }, + + // PRIVATE API + + /** +* Sets or remove (if handler is null) a handler in an object with a key. +* +* @param handler +* a handler, either function or an nsIHttpRequestHandler +* @param dict +* The object to attach the handler to. +* @param key +* The field name of the handler. +*/ + _handlerToField: function(handler, dict, key) + { + // for convenience, handler can be a function if this is run from xpcshell + if (typeof(handler) == "function") + dict[key] = handler; + else if (handler) + dict[key] = createHandlerFunc(handler); + else + delete dict[key]; + }, + + /** +* Handles a request which maps to a file in the local filesystem (if a base +* path has already been set; otherwise the 404 error is thrown). +* +* @param metadata : Request +* metadata for the incoming request +* @param response : Response +* an uninitialized Response to the given request, to be initialized by a +* request handler +* @throws HTTP_### +* if an HTTP error occurred (usually HTTP_404); note that in this case the +* calling code must handle post-processing of the response +*/ + _handleDefault: function(metadata, response) + { + dumpn("*** _handleDefault()"); + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + + var path = metadata.path; + NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">"); + + // determine the actual on-disk file; this requires finding the deepest + // path-to-directory mapping in the requested URL + var file = this._getFileForPath(path); + + // the "file" might be a directory, in which case we either serve the + // contained index.html or make the index handler write the response + if (file.exists() && file.isDirectory()) + { + file.append("index.html"); // make configurable? + if (!file.exists() || file.isDirectory()) + { + metadata._ensurePropertyBag(); + metadata._bag.setPropertyAsInterface("directory", file.parent); + this._indexHandler(metadata, response); + return; + } + } + + // alternately, the file might not exist + if (!file.exists()) + throw HTTP_404; + + var start, end; + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) && + metadata.hasHeader("Range") && + this._getTypeFromFile(file) !== SJS_TYPE) + { + var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/); + if (!rangeMatch) + throw HTTP_400; + + if (rangeMatch[1] !== undefined) + start = parseInt(rangeMatch[1], 10); + + if (rangeMatch[2] !== undefined) + end = parseInt(rangeMatch[2], 10); + + if (start === undefined && end === undefined) + throw HTTP_400; + + // No start given, so the end is really the count of bytes from the + // end of the file. + if (start === undefined) + { + start = Math.max(0, file.fileSize - end); + end = file.fileSize - 1; + } + + // start and end are inclusive + if (end === undefined || end >= file.fileSize) + end = file.fileSize - 1; + + if (start !== undefined && start >= file.fileSize) { + var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable"); + HTTP_416.customErrorHandling = function(errorResponse) + { + maybeAddHeaders(file, metadata, errorResponse); + }; + throw HTTP_416; + } + + if (end < start) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + start = 0; + end = file.fileSize - 1; + } + else + { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize; + response.setHeader("Content-Range", contentRange); + } + } + else + { + start = 0; + end = file.fileSize - 1; + } + + // finally... + dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " + + start + " to " + end + " inclusive"); + this._writeFileResponse(metadata, file, response, start, end - start + 1); + }, + + /** +* Writes an HTTP response for the given file, including setting headers for +* file metadata. +* +* @param metadata : Request +* the Request for which a response is being generated +* @param file : nsILocalFile +* the file which is to be sent in the response +* @param response : Response +* the response to which the file should be written +* @param offset: uint +* the byte offset to skip to when writing +* @param count: uint +* the number of bytes to write +*/ + _writeFileResponse: function(metadata, file, response, offset, count) + { + const PR_RDONLY = 0x01; + + var type = this._getTypeFromFile(file); + if (type === SJS_TYPE) + { + var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + try + { + var sis = new ScriptableInputStream(fis); + var s = Cu.Sandbox(gGlobalObject); + s.importFunction(dump, "dump"); + + // Define a basic key-value state-preservation API across requests, with + // keys initially corresponding to the empty string. + var self = this; + var path = metadata.path; + s.importFunction(function getState(k) + { + return self._getState(path, k); + }); + s.importFunction(function setState(k, v) + { + self._setState(path, k, v); + }); + s.importFunction(function getSharedState(k) + { + return self._getSharedState(k); + }); + s.importFunction(function setSharedState(k, v) + { + self._setSharedState(k, v); + }); + s.importFunction(function getObjectState(k, callback) + { + callback(self._getObjectState(k)); + }); + s.importFunction(function setObjectState(k, v) + { + self._setObjectState(k, v); + }); + s.importFunction(function registerPathHandler(p, h) + { + self.registerPathHandler(p, h); + }); + + // Make it possible for sjs files to access their location + this._setState(path, "__LOCATION__", file.path); + + try + { + // Alas, the line number in errors dumped to console when calling the + // request handler is simply an offset from where we load the SJS file. + // Work around this in a reasonably non-fragile way by dynamically + // getting the line number where we evaluate the SJS file. Don't + // separate these two lines! + var line = new Error().lineNumber; + Cu.evalInSandbox(sis.read(file.fileSize), s); + } + catch (e) + { + dumpn("*** syntax error in SJS at " + file.path + ": " + e); + throw HTTP_500; + } + + try + { + s.handleRequest(metadata, response); + } + catch (e) + { + dump("*** error running SJS at " + file.path + ": " + + e + " on line " + + (e instanceof Error + ? e.lineNumber + " in httpd.js" + : (e.lineNumber - line)) + "\n"); + throw HTTP_500; + } + } + finally + { + fis.close(); + } + } + else + { + try + { + response.setHeader("Last-Modified", + toDateString(file.lastModifiedTime), + false); + } + catch (e) { /* lastModifiedTime threw, ignore */ } + + response.setHeader("Content-Type", type, false); + maybeAddHeaders(file, metadata, response); + response.setHeader("Content-Length", "" + count, false); + + var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + offset = offset || 0; + count = count || file.fileSize; + NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset"); + NS_ASSERT(count >= 0, "bad count"); + NS_ASSERT(offset + count <= file.fileSize, "bad total data size"); + + try + { + if (offset !== 0) + { + // Seek (or read, if seeking isn't supported) to the correct offset so + // the data sent to the client matches the requested range. + if (fis instanceof Ci.nsISeekableStream) + fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset); + else + new ScriptableInputStream(fis).read(offset); + } + } + catch (e) + { + fis.close(); + throw e; + } + + let writeMore = function writeMore() + { + gThreadManager.currentThread + .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL); + } + + var input = new BinaryInputStream(fis); + var output = new BinaryOutputStream(response.bodyOutputStream); + var writeData = + { + run: function() + { + var chunkSize = Math.min(65536, count); + count -= chunkSize; + NS_ASSERT(count >= 0, "underflow"); + + try + { + var data = input.readByteArray(chunkSize); + NS_ASSERT(data.length === chunkSize, + "incorrect data returned? got " + data.length + + ", expected " + chunkSize); + output.writeByteArray(data, data.length); + if (count === 0) + { + fis.close(); + response.finish(); + } + else + { + writeMore(); + } + } + catch (e) + { + try + { + fis.close(); + } + finally + { + response.finish(); + } + throw e; + } + } + }; + + writeMore(); + + // Now that we know copying will start, flag the response as async. + response.processAsync(); + } + }, + + /** +* Get the value corresponding to a given key for the given path for SJS state +* preservation across requests. +* +* @param path : string +* the path from which the given state is to be retrieved +* @param k : string +* the key whose corresponding value is to be returned +* @returns string +* the corresponding value, which is initially the empty string +*/ + _getState: function(path, k) + { + var state = this._state; + if (path in state && k in state[path]) + return state[path][k]; + return ""; + }, + + /** +* Set the value corresponding to a given key for the given path for SJS state +* preservation across requests. +* +* @param path : string +* the path from which the given state is to be retrieved +* @param k : string +* the key whose corresponding value is to be set +* @param v : string +* the value to be set +*/ + _setState: function(path, k, v) + { + if (typeof v !== "string") + throw new Error("non-string value passed"); + var state = this._state; + if (!(path in state)) + state[path] = {}; + state[path][k] = v; + }, + + /** +* Get the value corresponding to a given key for SJS state preservation +* across requests. +* +* @param k : string +* the key whose corresponding value is to be returned +* @returns string +* the corresponding value, which is initially the empty string +*/ + _getSharedState: function(k) + { + var state = this._sharedState; + if (k in state) + return state[k]; + return ""; + }, + + /** +* Set the value corresponding to a given key for SJS state preservation +* across requests. +* +* @param k : string +* the key whose corresponding value is to be set +* @param v : string +* the value to be set +*/ + _setSharedState: function(k, v) + { + if (typeof v !== "string") + throw new Error("non-string value passed"); + this._sharedState[k] = v; + }, + + /** +* Returns the object associated with the given key in the server for SJS +* state preservation across requests. +* +* @param k : string +* the key whose corresponding object is to be returned +* @returns nsISupports +* the corresponding object, or null if none was present +*/ + _getObjectState: function(k) + { + if (typeof k !== "string") + throw new Error("non-string key passed"); + return this._objectState[k] || null; + }, + + /** +* Sets the object associated with the given key in the server for SJS +* state preservation across requests. +* +* @param k : string +* the key whose corresponding object is to be set +* @param v : nsISupports +* the object to be associated with the given key; may be null +*/ + _setObjectState: function(k, v) + { + if (typeof k !== "string") + throw new Error("non-string key passed"); + if (typeof v !== "object") + throw new Error("non-object value passed"); + if (v && !("QueryInterface" in v)) + { + throw new Error("must pass an nsISupports; use wrappedJSObject to ease " + + "pain when using the server from JS"); + } + + this._objectState[k] = v; + }, + + /** +* Gets a content-type for the given file, first by checking for any custom +* MIME-types registered with this handler for the file's extension, second by +* asking the global MIME service for a content-type, and finally by failing +* over to application/octet-stream. +* +* @param file : nsIFile +* the nsIFile for which to get a file type +* @returns string +* the best content-type which can be determined for the file +*/ + _getTypeFromFile: function(file) + { + try + { + var name = file.leafName; + var dot = name.lastIndexOf("."); + if (dot > 0) + { + var ext = name.slice(dot + 1); + if (ext in this._mimeMappings) + return this._mimeMappings[ext]; + } + return Cc["@mozilla.org/uriloader/external-helper-app-service;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(file); + } + catch (e) + { + return "application/octet-stream"; + } + }, + + /** +* Returns the nsILocalFile which corresponds to the path, as determined using +* all registered path->directory mappings and any paths which are explicitly +* overridden. +* +* @param path : string +* the server path for which a file should be retrieved, e.g. "/foo/bar" +* @throws HttpError +* when the correct action is the corresponding HTTP error (i.e., because no +* mapping was found for a directory in path, the referenced file doesn't +* exist, etc.) +* @returns nsILocalFile +* the file to be sent as the response to a request for the path +*/ + _getFileForPath: function(path) + { + // decode and add underscores as necessary + try + { + path = toInternalPath(path, true); + } + catch (e) + { + throw HTTP_400; // malformed path + } + + // next, get the directory which contains this path + var pathMap = this._pathDirectoryMap; + + // An example progression of tmp for a path "/foo/bar/baz/" might be: + // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", "" + var tmp = path.substring(1); + while (true) + { + // do we have a match for current head of the path? + var file = pathMap.get(tmp); + if (file) + { + // XXX hack; basically disable showing mapping for /foo/bar/ when the + // requested path was /foo/bar, because relative links on the page + // will all be incorrect -- we really need the ability to easily + // redirect here instead + if (tmp == path.substring(1) && + tmp.length != 0 && + tmp.charAt(tmp.length - 1) != "/") + file = null; + else + break; + } + + // if we've finished trying all prefixes, exit + if (tmp == "") + break; + + tmp = tmp.substring(0, tmp.lastIndexOf("/")); + } + + // no mapping applies, so 404 + if (!file) + throw HTTP_404; + + + // last, get the file for the path within the determined directory + var parentFolder = file.parent; + var dirIsRoot = (parentFolder == null); + + // Strategy here is to append components individually, making sure we + // never move above the given directory; this allows paths such as + // "/foo/../bar" but prevents paths such as "/../base-sibling"; + // this component-wise approach also means the code works even on platforms + // which don't use "/" as the directory separator, such as Windows + var leafPath = path.substring(tmp.length + 1); + var comps = leafPath.split("/"); + for (var i = 0, sz = comps.length; i < sz; i++) + { + var comp = comps[i]; + + if (comp == "..") + file = file.parent; + else if (comp == "." || comp == "") + continue; + else + file.append(comp); + + if (!dirIsRoot && file.equals(parentFolder)) + throw HTTP_403; + } + + return file; + }, + + /** +* Writes the error page for the given HTTP error code over the given +* connection. +* +* @param errorCode : uint +* the HTTP error code to be used +* @param connection : Connection +* the connection on which the error occurred +*/ + handleError: function(errorCode, connection) + { + var response = new Response(connection); + + dumpn("*** error in request: " + errorCode); + + this._handleError(errorCode, new Request(connection.port), response); + }, + + /** +* Handles a request which generates the given error code, using the +* user-defined error handler if one has been set, gracefully falling back to +* the x00 status code if the code has no handler, and failing to status code +* 500 if all else fails. +* +* @param errorCode : uint +* the HTTP error which is to be returned +* @param metadata : Request +* metadata for the request, which will often be incomplete since this is an +* error +* @param response : Response +* an uninitialized Response should be initialized when this method +* completes with information which represents the desired error code in the +* ideal case or a fallback code in abnormal circumstances (i.e., 500 is a +* fallback for 505, per HTTP specs) +*/ + _handleError: function(errorCode, metadata, response) + { + if (!metadata) + throw Cr.NS_ERROR_NULL_POINTER; + + var errorX00 = errorCode - (errorCode % 100); + + try + { + if (!(errorCode in HTTP_ERROR_CODES)) + dumpn("*** WARNING: requested invalid error: " + errorCode); + + // RFC 2616 says that we should try to handle an error by its class if we + // can't otherwise handle it -- if that fails, we revert to handling it as + // a 500 internal server error, and if that fails we throw and shut down + // the server + + // actually handle the error + try + { + if (errorCode in this._overrideErrors) + this._overrideErrors[errorCode](metadata, response); + else + this._defaultErrors[errorCode](metadata, response); + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(e); + return; + } + + // don't retry the handler that threw + if (errorX00 == errorCode) + throw HTTP_500; + + dumpn("*** error in handling for error code " + errorCode + ", " + + "falling back to " + errorX00 + "..."); + response = new Response(response._connection); + if (errorX00 in this._overrideErrors) + this._overrideErrors[errorX00](metadata, response); + else if (errorX00 in this._defaultErrors) + this._defaultErrors[errorX00](metadata, response); + else + throw HTTP_500; + } + } + catch (e) + { + if (response.partiallySent()) + { + response.abort(); + return; + } + + // we've tried everything possible for a meaningful error -- now try 500 + dumpn("*** error in handling for error code " + errorX00 + ", falling " + + "back to 500..."); + + try + { + response = new Response(response._connection); + if (500 in this._overrideErrors) + this._overrideErrors[500](metadata, response); + else + this._defaultErrors[500](metadata, response); + } + catch (e2) + { + dumpn("*** multiple errors in default error handlers!"); + dumpn("*** e == " + e + ", e2 == " + e2); + response.abort(e2); + return; + } + } + + response.complete(); + }, + + // FIELDS + + /** +* This object contains the default handlers for the various HTTP error codes. +*/ + _defaultErrors: + { + 400: function(metadata, response) + { + // none of the data in metadata is reliable, so hard-code everything here + response.setStatusLine("1.1", 400, "Bad Request"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Bad request\n"; + response.bodyOutputStream.write(body, body.length); + }, + 403: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 403, "Forbidden"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ +403 Forbidden\ +\ +

403 Forbidden

\ +\ +"; + response.bodyOutputStream.write(body, body.length); + }, + 404: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ +404 Not Found\ +\ +

404 Not Found

\ +

\ +" + + htmlEscape(metadata.path) + + " was not found.\ +

\ +\ +"; + response.bodyOutputStream.write(body, body.length); + }, + 416: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 416, + "Requested Range Not Satisfiable"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ +\ +416 Requested Range Not Satisfiable\ +\ +

416 Requested Range Not Satisfiable

\ +

The byte range was not valid for the\ +requested resource.\ +

\ +\ +"; + response.bodyOutputStream.write(body, body.length); + }, + 500: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, + 500, + "Internal Server Error"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ +500 Internal Server Error\ +\ +

500 Internal Server Error

\ +

Something's broken in this server and\ +needs to be fixed.

\ +\ +"; + response.bodyOutputStream.write(body, body.length); + }, + 501: function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 501, "Not Implemented"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ +501 Not Implemented\ +\ +

501 Not Implemented

\ +

This server is not (yet) Apache.

\ +\ +"; + response.bodyOutputStream.write(body, body.length); + }, + 505: function(metadata, response) + { + response.setStatusLine("1.1", 505, "HTTP Version Not Supported"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ +505 HTTP Version Not Supported\ +\ +

505 HTTP Version Not Supported

\ +

This server only supports HTTP/1.0 and HTTP/1.1\ +connections.

\ +\ +"; + response.bodyOutputStream.write(body, body.length); + } + }, + + /** +* Contains handlers for the default set of URIs contained in this server. +*/ + _defaultPaths: + { + "/": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + + var body = "\ +httpd.js\ +\ +

httpd.js

\ +

If you're seeing this page, httpd.js is up and\ +serving requests! Now set a base path and serve some\ +files!

\ +\ +"; + + response.bodyOutputStream.write(body, body.length); + }, + + "/trace": function(metadata, response) + { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + + var body = "Request-URI: " + + metadata.scheme + "://" + metadata.host + ":" + metadata.port + + metadata.path + "\n\n"; + body += "Request (semantically equivalent, slightly reformatted):\n\n"; + body += metadata.method + " " + metadata.path; + + if (metadata.queryString) + body += "?" + metadata.queryString; + + body += " HTTP/" + metadata.httpVersion + "\r\n"; + + var headEnum = metadata.headers; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n"; + } + + response.bodyOutputStream.write(body, body.length); + } + } +}; + + +/** +* Maps absolute paths to files on the local file system (as nsILocalFiles). +*/ +function FileMap() +{ + /** Hash which will map paths to nsILocalFiles. */ + this._map = {}; +} +FileMap.prototype = +{ + // PUBLIC API + + /** +* Maps key to a clone of the nsILocalFile value if value is non-null; +* otherwise, removes any extant mapping for key. +* +* @param key : string +* string to which a clone of value is mapped +* @param value : nsILocalFile +* the file to map to key, or null to remove a mapping +*/ + put: function(key, value) + { + if (value) + this._map[key] = value.clone(); + else + delete this._map[key]; + }, + + /** +* Returns a clone of the nsILocalFile mapped to key, or null if no such +* mapping exists. +* +* @param key : string +* key to which the returned file maps +* @returns nsILocalFile +* a clone of the mapped file, or null if no mapping exists +*/ + get: function(key) + { + var val = this._map[key]; + return val ? val.clone() : null; + } +}; + + +// Response CONSTANTS + +// token = * +// CHAR = +// CTL = +// separators = "(" | ")" | "<" | ">" | "@" +// | "," | ";" | ":" | "\" | <"> +// | "/" | "[" | "]" | "?" | "=" +// | "{" | "}" | SP | HT +const IS_TOKEN_ARRAY = + [0, 0, 0, 0, 0, 0, 0, 0, // 0 + 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 0, 0, 0, 0, 0, 0, 0, 0, // 16 + 0, 0, 0, 0, 0, 0, 0, 0, // 24 + + 0, 1, 0, 1, 1, 1, 1, 1, // 32 + 0, 0, 1, 1, 0, 1, 1, 0, // 40 + 1, 1, 1, 1, 1, 1, 1, 1, // 48 + 1, 1, 0, 0, 0, 0, 0, 0, // 56 + + 0, 1, 1, 1, 1, 1, 1, 1, // 64 + 1, 1, 1, 1, 1, 1, 1, 1, // 72 + 1, 1, 1, 1, 1, 1, 1, 1, // 80 + 1, 1, 1, 0, 0, 0, 1, 1, // 88 + + 1, 1, 1, 1, 1, 1, 1, 1, // 96 + 1, 1, 1, 1, 1, 1, 1, 1, // 104 + 1, 1, 1, 1, 1, 1, 1, 1, // 112 + 1, 1, 1, 0, 1, 0, 1]; // 120 + + +/** +* Determines whether the given character code is a CTL. +* +* @param code : uint +* the character code +* @returns boolean +* true if code is a CTL, false otherwise +*/ +function isCTL(code) +{ + return (code >= 0 && code <= 31) || (code == 127); +} + +/** +* Represents a response to an HTTP request, encapsulating all details of that +* response. This includes all headers, the HTTP version, status code and +* explanation, and the entity itself. +* +* @param connection : Connection +* the connection over which this response is to be written +*/ +function Response(connection) +{ + /** The connection over which this response will be written. */ + this._connection = connection; + + /** +* The HTTP version of this response; defaults to 1.1 if not set by the +* handler. +*/ + this._httpVersion = nsHttpVersion.HTTP_1_1; + + /** +* The HTTP code of this response; defaults to 200. +*/ + this._httpCode = 200; + + /** +* The description of the HTTP code in this response; defaults to "OK". +*/ + this._httpDescription = "OK"; + + /** +* An nsIHttpHeaders object in which the headers in this response should be +* stored. This property is null after the status line and headers have been +* written to the network, and it may be modified up until it is cleared, +* except if this._finished is set first (in which case headers are written +* asynchronously in response to a finish() call not preceded by +* flushHeaders()). +*/ + this._headers = new nsHttpHeaders(); + + /** +* Set to true when this response is ended (completely constructed if possible +* and the connection closed); further actions on this will then fail. +*/ + this._ended = false; + + /** +* A stream used to hold data written to the body of this response. +*/ + this._bodyOutputStream = null; + + /** +* A stream containing all data that has been written to the body of this +* response so far. (Async handlers make the data contained in this +* unreliable as a way of determining content length in general, but auxiliary +* saved information can sometimes be used to guarantee reliability.) +*/ + this._bodyInputStream = null; + + /** +* A stream copier which copies data to the network. It is initially null +* until replaced with a copier for response headers; when headers have been +* fully sent it is replaced with a copier for the response body, remaining +* so for the duration of response processing. +*/ + this._asyncCopier = null; + + /** +* True if this response has been designated as being processed +* asynchronously rather than for the duration of a single call to +* nsIHttpRequestHandler.handle. +*/ + this._processAsync = false; + + /** +* True iff finish() has been called on this, signaling that no more changes +* to this may be made. +*/ + this._finished = false; + + /** +* True iff powerSeized() has been called on this, signaling that this +* response is to be handled manually by the response handler (which may then +* send arbitrary data in response, even non-HTTP responses). +*/ + this._powerSeized = false; +} +Response.prototype = +{ + // PUBLIC CONSTRUCTION API + + // + // see nsIHttpResponse.bodyOutputStream + // + get bodyOutputStream() + { + if (this._finished) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + if (!this._bodyOutputStream) + { + var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX, + null); + this._bodyOutputStream = pipe.outputStream; + this._bodyInputStream = pipe.inputStream; + if (this._processAsync || this._powerSeized) + this._startAsyncProcessor(); + } + + return this._bodyOutputStream; + }, + + // + // see nsIHttpResponse.write + // + write: function(data) + { + if (this._finished) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + var dataAsString = String(data); + this.bodyOutputStream.write(dataAsString, dataAsString.length); + }, + + // + // see nsIHttpResponse.setStatusLine + // + setStatusLine: function(httpVersion, code, description) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + if (!(code >= 0 && code < 1000)) + throw Cr.NS_ERROR_INVALID_ARG; + + try + { + var httpVer; + // avoid version construction for the most common cases + if (!httpVersion || httpVersion == "1.1") + httpVer = nsHttpVersion.HTTP_1_1; + else if (httpVersion == "1.0") + httpVer = nsHttpVersion.HTTP_1_0; + else + httpVer = new nsHttpVersion(httpVersion); + } + catch (e) + { + throw Cr.NS_ERROR_INVALID_ARG; + } + + // Reason-Phrase = * + // TEXT = + // + // XXX this ends up disallowing octets which aren't Unicode, I think -- not + // much to do if description is IDL'd as string + if (!description) + description = ""; + for (var i = 0; i < description.length; i++) + if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t") + throw Cr.NS_ERROR_INVALID_ARG; + + // set the values only after validation to preserve atomicity + this._httpDescription = description; + this._httpCode = code; + this._httpVersion = httpVer; + }, + + // + // see nsIHttpResponse.setHeader + // + setHeader: function(name, value, merge) + { + if (!this._headers || this._finished || this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + this._ensureAlive(); + + this._headers.setHeader(name, value, merge); + }, + + // + // see nsIHttpResponse.processAsync + // + processAsync: function() + { + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._processAsync) + return; + this._ensureAlive(); + + dumpn("*** processing connection " + this._connection.number + " async"); + this._processAsync = true; + + /* +* Either the bodyOutputStream getter or this method is responsible for +* starting the asynchronous processor and catching writes of data to the +* response body of async responses as they happen, for the purpose of +* forwarding those writes to the actual connection's output stream. +* If bodyOutputStream is accessed first, calling this method will create +* the processor (when it first is clear that body data is to be written +* immediately, not buffered). If this method is called first, accessing +* bodyOutputStream will create the processor. If only this method is +* called, we'll write nothing, neither headers nor the nonexistent body, +* until finish() is called. Since that delay is easily avoided by simply +* getting bodyOutputStream or calling write(""), we don't worry about it. +*/ + if (this._bodyOutputStream && !this._asyncCopier) + this._startAsyncProcessor(); + }, + + // + // see nsIHttpResponse.seizePower + // + seizePower: function() + { + if (this._processAsync) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + return; + this._ensureAlive(); + + dumpn("*** forcefully seizing power over connection " + + this._connection.number + "..."); + + // Purge any already-written data without sending it. We could as easily + // swap out the streams entirely, but that makes it possible to acquire and + // unknowingly use a stale reference, so we require there only be one of + // each stream ever for any response to avoid this complication. + if (this._asyncCopier) + this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED); + this._asyncCopier = null; + if (this._bodyOutputStream) + { + var input = new BinaryInputStream(this._bodyInputStream); + var avail; + while ((avail = input.available()) > 0) + input.readByteArray(avail); + } + + this._powerSeized = true; + if (this._bodyOutputStream) + this._startAsyncProcessor(); + }, + + // + // see nsIHttpResponse.finish + // + finish: function() + { + if (!this._processAsync && !this._powerSeized) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._finished) + return; + + dumpn("*** finishing connection " + this._connection.number); + this._startAsyncProcessor(); // in case bodyOutputStream was never accessed + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + this._finished = true; + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // POST-CONSTRUCTION API (not exposed externally) + + /** +* The HTTP version number of this, as a string (e.g. "1.1"). +*/ + get httpVersion() + { + this._ensureAlive(); + return this._httpVersion.toString(); + }, + + /** +* The HTTP status code of this response, as a string of three characters per +* RFC 2616. +*/ + get httpCode() + { + this._ensureAlive(); + + var codeString = (this._httpCode < 10 ? "0" : "") + + (this._httpCode < 100 ? "0" : "") + + this._httpCode; + return codeString; + }, + + /** +* The description of the HTTP status code of this response, or "" if none is +* set. +*/ + get httpDescription() + { + this._ensureAlive(); + + return this._httpDescription; + }, + + /** +* The headers in this response, as an nsHttpHeaders object. +*/ + get headers() + { + this._ensureAlive(); + + return this._headers; + }, + + // + // see nsHttpHeaders.getHeader + // + getHeader: function(name) + { + this._ensureAlive(); + + return this._headers.getHeader(name); + }, + + /** +* Determines whether this response may be abandoned in favor of a newly +* constructed response. A response may be abandoned only if it is not being +* sent asynchronously and if raw control over it has not been taken from the +* server. +* +* @returns boolean +* true iff no data has been written to the network +*/ + partiallySent: function() + { + dumpn("*** partiallySent()"); + return this._processAsync || this._powerSeized; + }, + + /** +* If necessary, kicks off the remaining request processing needed to be done +* after a request handler performs its initial work upon this response. +*/ + complete: function() + { + dumpn("*** complete()"); + if (this._processAsync || this._powerSeized) + { + NS_ASSERT(this._processAsync ^ this._powerSeized, + "can't both send async and relinquish power"); + return; + } + + NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?"); + + this._startAsyncProcessor(); + + // Now make sure we finish processing this request! + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + }, + + /** +* Abruptly ends processing of this response, usually due to an error in an +* incoming request but potentially due to a bad error handler. Since we +* cannot handle the error in the usual way (giving an HTTP error page in +* response) because data may already have been sent (or because the response +* might be expected to have been generated asynchronously or completely from +* scratch by the handler), we stop processing this response and abruptly +* close the connection. +* +* @param e : Error +* the exception which precipitated this abort, or null if no such exception +* was generated +*/ + abort: function(e) + { + dumpn("*** abort(<" + e + ">)"); + + // This response will be ended by the processor if one was created. + var copier = this._asyncCopier; + if (copier) + { + // We dispatch asynchronously here so that any pending writes of data to + // the connection will be deterministically written. This makes it easier + // to specify exact behavior, and it makes observable behavior more + // predictable for clients. Note that the correctness of this depends on + // callbacks in response to _waitToReadData in WriteThroughCopier + // happening asynchronously with respect to the actual writing of data to + // bodyOutputStream, as they currently do; if they happened synchronously, + // an event which ran before this one could write more data to the + // response body before we get around to canceling the copier. We have + // tests for this in test_seizepower.js, however, and I can't think of a + // way to handle both cases without removing bodyOutputStream access and + // moving its effective write(data, length) method onto Response, which + // would be slower and require more code than this anyway. + gThreadManager.currentThread.dispatch({ + run: function() + { + dumpn("*** canceling copy asynchronously..."); + copier.cancel(Cr.NS_ERROR_UNEXPECTED); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + } + else + { + this.end(); + } + }, + + /** +* Closes this response's network connection, marks the response as finished, +* and notifies the server handler that the request is done being processed. +*/ + end: function() + { + NS_ASSERT(!this._ended, "ending this response twice?!?!"); + + this._connection.close(); + if (this._bodyOutputStream) + this._bodyOutputStream.close(); + + this._finished = true; + this._ended = true; + }, + + // PRIVATE IMPLEMENTATION + + /** +* Sends the status line and headers of this response if they haven't been +* sent and initiates the process of copying data written to this response's +* body to the network. +*/ + _startAsyncProcessor: function() + { + dumpn("*** _startAsyncProcessor()"); + + // Handle cases where we're being called a second time. The former case + // happens when this is triggered both by complete() and by processAsync(), + // while the latter happens when processAsync() in conjunction with sent + // data causes abort() to be called. + if (this._asyncCopier || this._ended) + { + dumpn("*** ignoring second call to _startAsyncProcessor"); + return; + } + + // Send headers if they haven't been sent already and should be sent, then + // asynchronously continue to send the body. + if (this._headers && !this._powerSeized) + { + this._sendHeaders(); + return; + } + + this._headers = null; + this._sendBody(); + }, + + /** +* Signals that all modifications to the response status line and headers are +* complete and then sends that data over the network to the client. Once +* this method completes, a different response to the request that resulted +* in this response cannot be sent -- the only possible action in case of +* error is to abort the response and close the connection. +*/ + _sendHeaders: function() + { + dumpn("*** _sendHeaders()"); + + NS_ASSERT(this._headers); + NS_ASSERT(!this._powerSeized); + + // request-line + var statusLine = "HTTP/" + this.httpVersion + " " + + this.httpCode + " " + + this.httpDescription + "\r\n"; + + // header post-processing + + var headers = this._headers; + headers.setHeader("Connection", "close", false); + headers.setHeader("Server", "httpd.js", false); + if (!headers.hasHeader("Date")) + headers.setHeader("Date", toDateString(Date.now()), false); + + // Any response not being processed asynchronously must have an associated + // Content-Length header for reasons of backwards compatibility with the + // initial server, which fully buffered every response before sending it. + // Beyond that, however, it's good to do this anyway because otherwise it's + // impossible to test behaviors that depend on the presence or absence of a + // Content-Length header. + if (!this._processAsync) + { + dumpn("*** non-async response, set Content-Length"); + + var bodyStream = this._bodyInputStream; + var avail = bodyStream ? bodyStream.available() : 0; + + // XXX assumes stream will always report the full amount of data available + headers.setHeader("Content-Length", "" + avail, false); + } + + + // construct and send response + dumpn("*** header post-processing completed, sending response head..."); + + // request-line + var preambleData = [statusLine]; + + // headers + var headEnum = headers.enumerator; + while (headEnum.hasMoreElements()) + { + var fieldName = headEnum.getNext() + .QueryInterface(Ci.nsISupportsString) + .data; + var values = headers.getHeaderValues(fieldName); + for (var i = 0, sz = values.length; i < sz; i++) + preambleData.push(fieldName + ": " + values[i] + "\r\n"); + } + + // end request-line/headers + preambleData.push("\r\n"); + + var preamble = preambleData.join(""); + + var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null); + responseHeadPipe.outputStream.write(preamble, preamble.length); + + var response = this; + var copyObserver = + { + onStartRequest: function(request, cx) + { + dumpn("*** preamble copying started"); + }, + + onStopRequest: function(request, cx, statusCode) + { + dumpn("*** preamble copying complete " + + "[status=0x" + statusCode.toString(16) + "]"); + + if (!components.isSuccessCode(statusCode)) + { + dumpn("!!! header copying problems: non-success statusCode, " + + "ending response"); + + response.end(); + } + else + { + response._sendBody(); + } + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + var headerCopier = this._asyncCopier = + new WriteThroughCopier(responseHeadPipe.inputStream, + this._connection.output, + copyObserver, null); + + responseHeadPipe.outputStream.close(); + + // Forbid setting any more headers or modifying the request line. + this._headers = null; + }, + + /** +* Asynchronously writes the body of the response (or the entire response, if +* seizePower() has been called) to the network. +*/ + _sendBody: function() + { + dumpn("*** _sendBody"); + + NS_ASSERT(!this._headers, "still have headers around but sending body?"); + + // If no body data was written, we're done + if (!this._bodyInputStream) + { + dumpn("*** empty body, response finished"); + this.end(); + return; + } + + var response = this; + var copyObserver = + { + onStartRequest: function(request, context) + { + dumpn("*** onStartRequest"); + }, + + onStopRequest: function(request, cx, statusCode) + { + dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]"); + + if (statusCode === Cr.NS_BINDING_ABORTED) + { + dumpn("*** terminating copy observer without ending the response"); + } + else + { + if (!components.isSuccessCode(statusCode)) + dumpn("*** WARNING: non-success statusCode in onStopRequest"); + + response.end(); + } + }, + + QueryInterface: function(aIID) + { + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + }; + + dumpn("*** starting async copier of body data..."); + this._asyncCopier = + new WriteThroughCopier(this._bodyInputStream, this._connection.output, + copyObserver, null); + }, + + /** Ensures that this hasn't been ended. */ + _ensureAlive: function() + { + NS_ASSERT(!this._ended, "not handling response lifetime correctly"); + } +}; + +/** +* Size of the segments in the buffer used in storing response data and writing +* it to the socket. +*/ +Response.SEGMENT_SIZE = 8192; + +/** Serves double duty in WriteThroughCopier implementation. */ +function notImplemented() +{ + throw Cr.NS_ERROR_NOT_IMPLEMENTED; +} + +/** Returns true iff the given exception represents stream closure. */ +function streamClosed(e) +{ + return e === Cr.NS_BASE_STREAM_CLOSED || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED); +} + +/** Returns true iff the given exception represents a blocked stream. */ +function wouldBlock(e) +{ + return e === Cr.NS_BASE_STREAM_WOULD_BLOCK || + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK); +} + +/** +* Copies data from source to sink as it becomes available, when that data can +* be written to sink without blocking. +* +* @param source : nsIAsyncInputStream +* the stream from which data is to be read +* @param sink : nsIAsyncOutputStream +* the stream to which data is to be copied +* @param observer : nsIRequestObserver +* an observer which will be notified when the copy starts and finishes +* @param context : nsISupports +* context passed to observer when notified of start/stop +* @throws NS_ERROR_NULL_POINTER +* if source, sink, or observer are null +*/ +function WriteThroughCopier(source, sink, observer, context) +{ + if (!source || !sink || !observer) + throw Cr.NS_ERROR_NULL_POINTER; + + /** Stream from which data is being read. */ + this._source = source; + + /** Stream to which data is being written. */ + this._sink = sink; + + /** Observer watching this copy. */ + this._observer = observer; + + /** Context for the observer watching this. */ + this._context = context; + + /** +* True iff this is currently being canceled (cancel has been called, the +* callback may not yet have been made). +*/ + this._canceled = false; + + /** +* False until all data has been read from input and written to output, at +* which point this copy is completed and cancel() is asynchronously called. +*/ + this._completed = false; + + /** Required by nsIRequest, meaningless. */ + this.loadFlags = 0; + /** Required by nsIRequest, meaningless. */ + this.loadGroup = null; + /** Required by nsIRequest, meaningless. */ + this.name = "response-body-copy"; + + /** Status of this request. */ + this.status = Cr.NS_OK; + + /** Arrays of byte strings waiting to be written to output. */ + this._pendingData = []; + + // start copying + try + { + observer.onStartRequest(this, context); + this._waitToReadData(); + this._waitForSinkClosure(); + } + catch (e) + { + dumpn("!!! error starting copy: " + e + + ("lineNumber" in e ? ", line " + e.lineNumber : "")); + dumpn(e.stack); + this.cancel(Cr.NS_ERROR_UNEXPECTED); + } +} +WriteThroughCopier.prototype = +{ + /* nsISupports implementation */ + + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIInputStreamCallback) || + iid.equals(Ci.nsIOutputStreamCallback) || + iid.equals(Ci.nsIRequest) || + iid.equals(Ci.nsISupports)) + { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // NSIINPUTSTREAMCALLBACK + + /** +* Receives a more-data-in-input notification and writes the corresponding +* data to the output. +* +* @param input : nsIAsyncInputStream +* the input stream on whose data we have been waiting +*/ + onInputStreamReady: function(input) + { + if (this._source === null) + return; + + dumpn("*** onInputStreamReady"); + + // + // Ordinarily we'll read a non-zero amount of data from input, queue it up + // to be written and then wait for further callbacks. The complications in + // this method are the cases where we deviate from that behavior when errors + // occur or when copying is drawing to a finish. + // + // The edge cases when reading data are: + // + // Zero data is read + // If zero data was read, we're at the end of available data, so we can + // should stop reading and move on to writing out what we have (or, if + // we've already done that, onto notifying of completion). + // A stream-closed exception is thrown + // This is effectively a less kind version of zero data being read; the + // only difference is that we notify of completion with that result + // rather than with NS_OK. + // Some other exception is thrown + // This is the least kind result. We don't know what happened, so we + // act as though the stream closed except that we notify of completion + // with the result NS_ERROR_UNEXPECTED. + // + + var bytesWanted = 0, bytesConsumed = -1; + try + { + input = new BinaryInputStream(input); + + bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE); + dumpn("*** input wanted: " + bytesWanted); + + if (bytesWanted > 0) + { + var data = input.readByteArray(bytesWanted); + bytesConsumed = data.length; + this._pendingData.push(String.fromCharCode.apply(String, data)); + } + + dumpn("*** " + bytesConsumed + " bytes read"); + + // Handle the zero-data edge case in the same place as all other edge + // cases are handled. + if (bytesWanted === 0) + throw Cr.NS_BASE_STREAM_CLOSED; + } + catch (e) + { + if (streamClosed(e)) + { + dumpn("*** input stream closed"); + e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED; + } + else + { + dumpn("!!! unexpected error reading from input, canceling: " + e); + e = Cr.NS_ERROR_UNEXPECTED; + } + + this._doneReadingSource(e); + return; + } + + var pendingData = this._pendingData; + + NS_ASSERT(bytesConsumed > 0); + NS_ASSERT(pendingData.length > 0, "no pending data somehow?"); + NS_ASSERT(pendingData[pendingData.length - 1].length > 0, + "buffered zero bytes of data?"); + + NS_ASSERT(this._source !== null); + + // Reading has gone great, and we've gotten data to write now. What if we + // don't have a place to write that data, because output went away just + // before this read? Drop everything on the floor, including new data, and + // cancel at this point. + if (this._sink === null) + { + pendingData.length = 0; + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we've read the data, and we know we have a place to write it. We + // need to queue up the data to be written, but *only* if none is queued + // already -- if data's already queued, the code that actually writes the + // data will make sure to wait on unconsumed pending data. + try + { + if (pendingData.length === 1) + this._waitToWriteData(); + } + catch (e) + { + dumpn("!!! error waiting to write data just read, swallowing and " + + "writing only what we already have: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Whee! We successfully read some data, and it's successfully queued up to + // be written. All that remains now is to wait for more data to read. + try + { + this._waitToReadData(); + } + catch (e) + { + dumpn("!!! error waiting to read more data: " + e); + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); + } + }, + + + // NSIOUTPUTSTREAMCALLBACK + + /** +* Callback when data may be written to the output stream without blocking, or +* when the output stream has been closed. +* +* @param output : nsIAsyncOutputStream +* the output stream on whose writability we've been waiting, also known as +* this._sink +*/ + onOutputStreamReady: function(output) + { + if (this._sink === null) + return; + + dumpn("*** onOutputStreamReady"); + + var pendingData = this._pendingData; + if (pendingData.length === 0) + { + // There's no pending data to write. The only way this can happen is if + // we're waiting on the output stream's closure, so we can respond to a + // copying failure as quickly as possible (rather than waiting for data to + // be available to read and then fail to be copied). Therefore, we must + // be done now -- don't bother to attempt to write anything and wrap + // things up. + dumpn("!!! output stream closed prematurely, ending copy"); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + + NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?"); + + // + // Write out the first pending quantum of data. The possible errors here + // are: + // + // The write might fail because we can't write that much data + // Okay, we've written what we can now, so re-queue what's left and + // finish writing it out later. + // The write failed because the stream was closed + // Discard pending data that we can no longer write, stop reading, and + // signal that copying finished. + // Some other error occurred. + // Same as if the stream were closed, but notify with the status + // NS_ERROR_UNEXPECTED so the observer knows something was wonky. + // + + try + { + var quantum = pendingData[0]; + + // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on + // undefined behavior! We're only using this because writeByteArray + // is unusably broken for asynchronous output streams; see bug 532834 + // for details. + var bytesWritten = output.write(quantum, quantum.length); + if (bytesWritten === quantum.length) + pendingData.shift(); + else + pendingData[0] = quantum.substring(bytesWritten); + + dumpn("*** wrote " + bytesWritten + " bytes of data"); + } + catch (e) + { + if (wouldBlock(e)) + { + NS_ASSERT(pendingData.length > 0, + "stream-blocking exception with no data to write?"); + NS_ASSERT(pendingData[0].length > 0, + "stream-blocking exception with empty quantum?"); + this._waitToWriteData(); + return; + } + + if (streamClosed(e)) + dumpn("!!! output stream prematurely closed, signaling error..."); + else + dumpn("!!! unknown error: " + e + ", quantum=" + quantum); + + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // The day is ours! Quantum written, now let's see if we have more data + // still to write. + try + { + if (pendingData.length > 0) + { + this._waitToWriteData(); + return; + } + } + catch (e) + { + dumpn("!!! unexpected error waiting to write pending data: " + e); + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); + return; + } + + // Okay, we have no more pending data to write -- but might we get more in + // the future? + if (this._source !== null) + { + /* +* If we might, then wait for the output stream to be closed. (We wait +* only for closure because we have no data to write -- and if we waited +* for a specific amount of data, we would get repeatedly notified for no +* reason if over time the output stream permitted more and more data to +* be written to it without blocking.) +*/ + this._waitForSinkClosure(); + } + else + { + /* +* On the other hand, if we can't have more data because the input +* stream's gone away, then it's time to notify of copy completion. +* Victory! +*/ + this._sink = null; + this._cancelOrDispatchCancelCallback(Cr.NS_OK); + } + }, + + + // NSIREQUEST + + /** Returns true if the cancel observer hasn't been notified yet. */ + isPending: function() + { + return !this._completed; + }, + + /** Not implemented, don't use! */ + suspend: notImplemented, + /** Not implemented, don't use! */ + resume: notImplemented, + + /** +* Cancels data reading from input, asynchronously writes out any pending +* data, and causes the observer to be notified with the given error code when +* all writing has finished. +* +* @param status : nsresult +* the status to pass to the observer when data copying has been canceled +*/ + cancel: function(status) + { + dumpn("*** cancel(" + status.toString(16) + ")"); + + if (this._canceled) + { + dumpn("*** suppressing a late cancel"); + return; + } + + this._canceled = true; + this.status = status; + + // We could be in the middle of absolutely anything at this point. Both + // input and output might still be around, we might have pending data to + // write, and in general we know nothing about the state of the world. We + // therefore must assume everything's in progress and take everything to its + // final steady state (or so far as it can go before we need to finish + // writing out remaining data). + + this._doneReadingSource(status); + }, + + + // PRIVATE IMPLEMENTATION + + /** +* Stop reading input if we haven't already done so, passing e as the status +* when closing the stream, and kick off a copy-completion notice if no more +* data remains to be written. +* +* @param e : nsresult +* the status to be used when closing the input stream +*/ + _doneReadingSource: function(e) + { + dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")"); + + this._finishSource(e); + if (this._pendingData.length === 0) + this._sink = null; + else + NS_ASSERT(this._sink !== null, "null output?"); + + // If we've written out all data read up to this point, then it's time to + // signal completion. + if (this._sink === null) + { + NS_ASSERT(this._pendingData.length === 0, "pending data still?"); + this._cancelOrDispatchCancelCallback(e); + } + }, + + /** +* Stop writing output if we haven't already done so, discard any data that +* remained to be sent, close off input if it wasn't already closed, and kick +* off a copy-completion notice. +* +* @param e : nsresult +* the status to be used when closing input if it wasn't already closed +*/ + _doneWritingToSink: function(e) + { + dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")"); + + this._pendingData.length = 0; + this._sink = null; + this._doneReadingSource(e); + }, + + /** +* Completes processing of this copy: either by canceling the copy if it +* hasn't already been canceled using the provided status, or by dispatching +* the cancel callback event (with the originally provided status, of course) +* if it already has been canceled. +* +* @param status : nsresult +* the status code to use to cancel this, if this hasn't already been +* canceled +*/ + _cancelOrDispatchCancelCallback: function(status) + { + dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")"); + + NS_ASSERT(this._source === null, "should have finished input"); + NS_ASSERT(this._sink === null, "should have finished output"); + NS_ASSERT(this._pendingData.length === 0, "should have no pending data"); + + if (!this._canceled) + { + this.cancel(status); + return; + } + + var self = this; + var event = + { + run: function() + { + dumpn("*** onStopRequest async callback"); + + self._completed = true; + try + { + self._observer.onStopRequest(self, self._context, self.status); + } + catch (e) + { + NS_ASSERT(false, + "how are we throwing an exception here? we control " + + "all the callers! " + e); + } + } + }; + + gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** +* Kicks off another wait for more data to be available from the input stream. +*/ + _waitToReadData: function() + { + dumpn("*** _waitToReadData"); + this._source.asyncWait(this, 0, Response.SEGMENT_SIZE, + gThreadManager.mainThread); + }, + + /** +* Kicks off another wait until data can be written to the output stream. +*/ + _waitToWriteData: function() + { + dumpn("*** _waitToWriteData"); + + var pendingData = this._pendingData; + NS_ASSERT(pendingData.length > 0, "no pending data to write?"); + NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?"); + + this._sink.asyncWait(this, 0, pendingData[0].length, + gThreadManager.mainThread); + }, + + /** +* Kicks off a wait for the sink to which data is being copied to be closed. +* We wait for stream closure when we don't have any data to be copied, rather +* than waiting to write a specific amount of data. We can't wait to write +* data because the sink might be infinitely writable, and if no data appears +* in the source for a long time we might have to spin quite a bit waiting to +* write, waiting to write again, &c. Waiting on stream closure instead means +* we'll get just one notification if the sink dies. Note that when data +* starts arriving from the sink we'll resume waiting for data to be written, +* dropping this closure-only callback entirely. +*/ + _waitForSinkClosure: function() + { + dumpn("*** _waitForSinkClosure"); + + this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0, + gThreadManager.mainThread); + }, + + /** +* Closes input with the given status, if it hasn't already been closed; +* otherwise a no-op. +* +* @param status : nsresult +* status code use to close the source stream if necessary +*/ + _finishSource: function(status) + { + dumpn("*** _finishSource(" + status.toString(16) + ")"); + + if (this._source !== null) + { + this._source.closeWithStatus(status); + this._source = null; + } + } +}; + + +/** +* A container for utility functions used with HTTP headers. +*/ +const headerUtils = +{ + /** +* Normalizes fieldName (by converting it to lowercase) and ensures it is a +* valid header field name (although not necessarily one specified in RFC +* 2616). +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not match the field-name production in RFC 2616 +* @returns string +* fieldName converted to lowercase if it is a valid header, for characters +* where case conversion is possible +*/ + normalizeFieldName: function(fieldName) + { + if (fieldName == "") + throw Cr.NS_ERROR_INVALID_ARG; + + for (var i = 0, sz = fieldName.length; i < sz; i++) + { + if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)]) + { + dumpn(fieldName + " is not a valid header field name!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + } + + return fieldName.toLowerCase(); + }, + + /** +* Ensures that fieldValue is a valid header field value (although not +* necessarily as specified in RFC 2616 if the corresponding field name is +* part of the HTTP protocol), normalizes the value if it is, and +* returns the normalized value. +* +* @param fieldValue : string +* a value to be normalized as an HTTP header field value +* @throws NS_ERROR_INVALID_ARG +* if fieldValue does not match the field-value production in RFC 2616 +* @returns string +* fieldValue as a normalized HTTP header field value +*/ + normalizeFieldValue: function(fieldValue) + { + // field-value = *( field-content | LWS ) + // field-content = + // TEXT = + // LWS = [CRLF] 1*( SP | HT ) + // + // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + // qdtext = > + // quoted-pair = "\" CHAR + // CHAR = + + // Any LWS that occurs between field-content MAY be replaced with a single + // SP before interpreting the field value or forwarding the message + // downstream (section 4.2); we replace 1*LWS with a single SP + var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " "); + + // remove leading/trailing LWS (which has been converted to SP) + val = val.replace(/^ +/, "").replace(/ +$/, ""); + + // that should have taken care of all CTLs, so val should contain no CTLs + for (var i = 0, len = val.length; i < len; i++) + if (isCTL(val.charCodeAt(i))) + throw Cr.NS_ERROR_INVALID_ARG; + + // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly + // normalize, however, so this can be construed as a tightening of the + // spec and not entirely as a bug + return val; + } +}; + + + +/** +* Converts the given string into a string which is safe for use in an HTML +* context. +* +* @param str : string +* the string to make HTML-safe +* @returns string +* an HTML-safe version of str +*/ +function htmlEscape(str) +{ + // this is naive, but it'll work + var s = ""; + for (var i = 0; i < str.length; i++) + s += "&#" + str.charCodeAt(i) + ";"; + return s; +} + + +/** +* Constructs an object representing an HTTP version (see section 3.1). +* +* @param versionString +* a string of the form "#.#", where # is an non-negative decimal integer with +* or without leading zeros +* @throws +* if versionString does not specify a valid HTTP version number +*/ +function nsHttpVersion(versionString) +{ + var matches = /^(\d+)\.(\d+)$/.exec(versionString); + if (!matches) + throw "Not a valid HTTP version!"; + + /** The major version number of this, as a number. */ + this.major = parseInt(matches[1], 10); + + /** The minor version number of this, as a number. */ + this.minor = parseInt(matches[2], 10); + + if (isNaN(this.major) || isNaN(this.minor) || + this.major < 0 || this.minor < 0) + throw "Not a valid HTTP version!"; +} +nsHttpVersion.prototype = +{ + /** +* Returns the standard string representation of the HTTP version represented +* by this (e.g., "1.1"). +*/ + toString: function () + { + return this.major + "." + this.minor; + }, + + /** +* Returns true if this represents the same HTTP version as otherVersion, +* false otherwise. +* +* @param otherVersion : nsHttpVersion +* the version to compare against this +*/ + equals: function (otherVersion) + { + return this.major == otherVersion.major && + this.minor == otherVersion.minor; + }, + + /** True if this >= otherVersion, false otherwise. */ + atLeast: function(otherVersion) + { + return this.major > otherVersion.major || + (this.major == otherVersion.major && + this.minor >= otherVersion.minor); + } +}; + +nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0"); +nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1"); + + +/** +* An object which stores HTTP headers for a request or response. +* +* Note that since headers are case-insensitive, this object converts headers to +* lowercase before storing them. This allows the getHeader and hasHeader +* methods to work correctly for any case of a header, but it means that the +* values returned by .enumerator may not be equal case-sensitively to the +* values passed to setHeader when adding headers to this. +*/ +function nsHttpHeaders() +{ + /** +* A hash of headers, with header field names as the keys and header field +* values as the values. Header field names are case-insensitive, but upon +* insertion here they are converted to lowercase. Header field values are +* normalized upon insertion to contain no leading or trailing whitespace. +* +* Note also that per RFC 2616, section 4.2, two headers with the same name in +* a message may be treated as one header with the same field name and a field +* value consisting of the separate field values joined together with a "," in +* their original order. This hash stores multiple headers with the same name +* in this manner. +*/ + this._headers = {}; +} +nsHttpHeaders.prototype = +{ + /** +* Sets the header represented by name and value in this. +* +* @param name : string +* the header name +* @param value : string +* the header value +* @throws NS_ERROR_INVALID_ARG +* if name or value is not a valid header component +*/ + setHeader: function(fieldName, fieldValue, merge) + { + var name = headerUtils.normalizeFieldName(fieldName); + var value = headerUtils.normalizeFieldValue(fieldValue); + + // The following three headers are stored as arrays because their real-world + // syntax prevents joining individual headers into a single header using + // ",". See also + if (merge && name in this._headers) + { + if (name === "www-authenticate" || + name === "proxy-authenticate" || + name === "set-cookie") + { + this._headers[name].push(value); + } + else + { + this._headers[name][0] += "," + value; + NS_ASSERT(this._headers[name].length === 1, + "how'd a non-special header have multiple values?") + } + } + else + { + this._headers[name] = [value]; + } + }, + + /** +* Returns the value for the header specified by this. +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @throws NS_ERROR_NOT_AVAILABLE +* if the given header does not exist in this +* @returns string +* the field value for the given header, possibly with non-semantic changes +* (i.e., leading/trailing whitespace stripped, whitespace runs replaced +* with spaces, etc.) at the option of the implementation; multiple +* instances of the header will be combined with a comma, except for +* the three headers noted in the description of getHeaderValues +*/ + getHeader: function(fieldName) + { + return this.getHeaderValues(fieldName).join("\n"); + }, + + /** +* Returns the value for the header specified by fieldName as an array. +* +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @throws NS_ERROR_NOT_AVAILABLE +* if the given header does not exist in this +* @returns [string] +* an array of all the header values in this for the given +* header name. Header values will generally be collapsed +* into a single header by joining all header values together +* with commas, but certain headers (Proxy-Authenticate, +* WWW-Authenticate, and Set-Cookie) violate the HTTP spec +* and cannot be collapsed in this manner. For these headers +* only, the returned array may contain multiple elements if +* that header has been added more than once. +*/ + getHeaderValues: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + + if (name in this._headers) + return this._headers[name]; + else + throw Cr.NS_ERROR_NOT_AVAILABLE; + }, + + /** +* Returns true if a header with the given field name exists in this, false +* otherwise. +* +* @param fieldName : string +* the field name whose existence is to be determined in this +* @throws NS_ERROR_INVALID_ARG +* if fieldName does not constitute a valid header field name +* @returns boolean +* true if the header's present, false otherwise +*/ + hasHeader: function(fieldName) + { + var name = headerUtils.normalizeFieldName(fieldName); + return (name in this._headers); + }, + + /** +* Returns a new enumerator over the field names of the headers in this, as +* nsISupportsStrings. The names returned will be in lowercase, regardless of +* how they were input using setHeader (header names are case-insensitive per +* RFC 2616). +*/ + get enumerator() + { + var headers = []; + for (var i in this._headers) + { + var supports = new SupportsString(); + supports.data = i; + headers.push(supports); + } + + return new nsSimpleEnumerator(headers); + } +}; + + +/** +* Constructs an nsISimpleEnumerator for the given array of items. +* +* @param items : Array +* the items, which must all implement nsISupports +*/ +function nsSimpleEnumerator(items) +{ + this._items = items; + this._nextIndex = 0; +} +nsSimpleEnumerator.prototype = +{ + hasMoreElements: function() + { + return this._nextIndex < this._items.length; + }, + getNext: function() + { + if (!this.hasMoreElements()) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + return this._items[this._nextIndex++]; + }, + QueryInterface: function(aIID) + { + if (Ci.nsISimpleEnumerator.equals(aIID) || + Ci.nsISupports.equals(aIID)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + + +/** +* A representation of the data in an HTTP request. +* +* @param port : uint +* the port on which the server receiving this request runs +*/ +function Request(port) +{ + /** Method of this request, e.g. GET or POST. */ + this._method = ""; + + /** Path of the requested resource; empty paths are converted to '/'. */ + this._path = ""; + + /** Query string, if any, associated with this request (not including '?'). */ + this._queryString = ""; + + /** Scheme of requested resource, usually http, always lowercase. */ + this._scheme = "http"; + + /** Hostname on which the requested resource resides. */ + this._host = undefined; + + /** Port number over which the request was received. */ + this._port = port; + + var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null); + + /** Stream from which data in this request's body may be read. */ + this._bodyInputStream = bodyPipe.inputStream; + + /** Stream to which data in this request's body is written. */ + this._bodyOutputStream = bodyPipe.outputStream; + + /** +* The headers in this request. +*/ + this._headers = new nsHttpHeaders(); + + /** +* For the addition of ad-hoc properties and new functionality without having +* to change nsIHttpRequest every time; currently lazily created, as its only +* use is in directory listings. +*/ + this._bag = null; +} +Request.prototype = +{ + // SERVER METADATA + + // + // see nsIHttpRequest.scheme + // + get scheme() + { + return this._scheme; + }, + + // + // see nsIHttpRequest.host + // + get host() + { + return this._host; + }, + + // + // see nsIHttpRequest.port + // + get port() + { + return this._port; + }, + + // REQUEST LINE + + // + // see nsIHttpRequest.method + // + get method() + { + return this._method; + }, + + // + // see nsIHttpRequest.httpVersion + // + get httpVersion() + { + return this._httpVersion.toString(); + }, + + // + // see nsIHttpRequest.path + // + get path() + { + return this._path; + }, + + // + // see nsIHttpRequest.queryString + // + get queryString() + { + return this._queryString; + }, + + // HEADERS + + // + // see nsIHttpRequest.getHeader + // + getHeader: function(name) + { + return this._headers.getHeader(name); + }, + + // + // see nsIHttpRequest.hasHeader + // + hasHeader: function(name) + { + return this._headers.hasHeader(name); + }, + + // + // see nsIHttpRequest.headers + // + get headers() + { + return this._headers.enumerator; + }, + + // + // see nsIPropertyBag.enumerator + // + get enumerator() + { + this._ensurePropertyBag(); + return this._bag.enumerator; + }, + + // + // see nsIHttpRequest.headers + // + get bodyInputStream() + { + return this._bodyInputStream; + }, + + // + // see nsIPropertyBag.getProperty + // + getProperty: function(name) + { + this._ensurePropertyBag(); + return this._bag.getProperty(name); + }, + + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + + // PRIVATE IMPLEMENTATION + + /** Ensures a property bag has been created for ad-hoc behaviors. */ + _ensurePropertyBag: function() + { + if (!this._bag) + this._bag = new WritablePropertyBag(); + } +}; + + +// XPCOM trappings +if ("XPCOMUtils" in this && // Firefox 3.6 doesn't load XPCOMUtils in this scope for some reason... + "generateNSGetFactory" in XPCOMUtils) { + var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]); +} + + + +/** +* Creates a new HTTP server listening for loopback traffic on the given port, +* starts it, and runs the server until the server processes a shutdown request, +* spinning an event loop so that events posted by the server's socket are +* processed. +* +* This method is primarily intended for use in running this script from within +* xpcshell and running a functional HTTP server without having to deal with +* non-essential details. +* +* Note that running multiple servers using variants of this method probably +* doesn't work, simply due to how the internal event loop is spun and stopped. +* +* @note +* This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code); +* you should use this server as a component in Mozilla 1.8. +* @param port +* the port on which the server will run, or -1 if there exists no preference +* for a specific port; note that attempting to use some values for this +* parameter (particularly those below 1024) may cause this method to throw or +* may result in the server being prematurely shut down +* @param basePath +* a local directory from which requests will be served (i.e., if this is +* "/home/jwalden/" then a request to /index.html will load +* /home/jwalden/index.html); if this is omitted, only the default URLs in +* this server implementation will be functional +*/ +function server(port, basePath) +{ + if (basePath) + { + var lp = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + lp.initWithPath(basePath); + } + + // if you're running this, you probably want to see debugging info + DEBUG = true; + + var srv = new nsHttpServer(); + if (lp) + srv.registerDirectory("/", lp); + srv.registerContentType("sjs", SJS_TYPE); + srv.start(port); + + var thread = gThreadManager.currentThread; + while (!srv.isStopped()) + thread.processNextEvent(true); + + // get rid of any pending requests + while (thread.hasPendingEvents()) + thread.processNextEvent(true); + + DEBUG = false; +} + +function startServerAsync(port, basePath) +{ + if (basePath) + { + var lp = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + lp.initWithPath(basePath); + } + + var srv = new nsHttpServer(); + if (lp) + srv.registerDirectory("/", lp); + srv.registerContentType("sjs", "sjs"); + srv.start(port); + return srv; +} + +exports.nsHttpServer = nsHttpServer; +exports.ScriptableInputStream = ScriptableInputStream; +exports.server = server; +exports.startServerAsync = startServerAsync; diff --git a/addon-sdk/source/test/addons/e10s-content/lib/main.js b/addon-sdk/source/test/addons/e10s-content/lib/main.js new file mode 100644 index 000000000000..6ca7dc48c434 --- /dev/null +++ b/addon-sdk/source/test/addons/e10s-content/lib/main.js @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +const { merge } = require('sdk/util/object'); +const { version } = require('sdk/system'); + +const SKIPPING_TESTS = { + "test skip": (assert) => assert.pass("nothing to test here") +}; + +merge(module.exports, require('./test-content-script')); +merge(module.exports, require('./test-content-worker')); +merge(module.exports, require('./test-page-worker')); + +// run e10s tests only on builds from trunk, fx-team, Nightly.. +if (!version.endsWith('a1')) { + module.exports = SKIPPING_TESTS; +} + +require('sdk/test/runner').runTestsFromModule(module); diff --git a/addon-sdk/source/test/addons/e10s-content/lib/test-content-script.js b/addon-sdk/source/test/addons/e10s-content/lib/test-content-script.js new file mode 100644 index 000000000000..0e050f866732 --- /dev/null +++ b/addon-sdk/source/test/addons/e10s-content/lib/test-content-script.js @@ -0,0 +1,856 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const hiddenFrames = require("sdk/frame/hidden-frame"); +const { create: makeFrame } = require("sdk/frame/utils"); +const { window } = require("sdk/addon/window"); +const { Loader } = require('sdk/test/loader'); +const { URL } = require("sdk/url"); +const testURI = require("./fixtures").url("test.html"); +const testHost = URL(testURI).scheme + '://' + URL(testURI).host; + +/* + * Utility function that allow to easily run a proxy test with a clean + * new HTML document. See first unit test for usage. + */ +function createProxyTest(html, callback) { + return function (assert, done) { + let url = 'data:text/html;charset=utf-8,' + encodeURIComponent(html); + let principalLoaded = false; + + let element = makeFrame(window.document, { + nodeName: "iframe", + type: "content", + allowJavascript: true, + allowPlugins: true, + allowAuth: true, + uri: testURI + }); + + element.addEventListener("DOMContentLoaded", onDOMReady, false); + + function onDOMReady() { + // Reload frame after getting principal from `testURI` + if (!principalLoaded) { + element.setAttribute("src", url); + principalLoaded = true; + return; + } + + assert.equal(element.getAttribute("src"), url, "correct URL loaded"); + element.removeEventListener("DOMContentLoaded", onDOMReady, + false); + let xrayWindow = element.contentWindow; + let rawWindow = xrayWindow.wrappedJSObject; + + let isDone = false; + let helper = { + xrayWindow: xrayWindow, + rawWindow: rawWindow, + createWorker: function (contentScript) { + return createWorker(assert, xrayWindow, contentScript, helper.done); + }, + done: function () { + if (isDone) + return; + isDone = true; + element.parentNode.removeChild(element); + done(); + } + }; + callback(helper, assert); + } + }; +} + +function createWorker(assert, xrayWindow, contentScript, done) { + let loader = Loader(module); + let Worker = loader.require("sdk/content/worker").Worker; + let worker = Worker({ + window: xrayWindow, + contentScript: [ + 'new ' + function () { + assert = function assert(v, msg) { + self.port.emit("assert", {assertion:v, msg:msg}); + } + done = function done() { + self.port.emit("done"); + } + }, + contentScript + ] + }); + + worker.port.on("done", done); + worker.port.on("assert", function (data) { + assert.ok(data.assertion, data.msg); + }); + + return worker; +} + +/* Examples for the `createProxyTest` uses */ + +let html = ""; + +exports["test Create Proxy Test"] = createProxyTest(html, function (helper, assert) { + // You can get access to regular `test` object in second argument of + // `createProxyTest` method: + assert.ok(helper.rawWindow.documentGlobal, + "You have access to a raw window reference via `helper.rawWindow`"); + assert.ok(!("documentGlobal" in helper.xrayWindow), + "You have access to an XrayWrapper reference via `helper.xrayWindow`"); + + // If you do not create a Worker, you have to call helper.done(), + // in order to say when your test is finished + helper.done(); +}); + +exports["test Create Proxy Test With Worker"] = createProxyTest("", function (helper) { + + helper.createWorker( + "new " + function WorkerScope() { + assert(true, "You can do assertions in your content script"); + // And if you create a worker, you either have to call `done` + // from content script or helper.done() + done(); + } + ); + +}); + +exports["test Create Proxy Test With Events"] = createProxyTest("", function (helper, assert) { + + let worker = helper.createWorker( + "new " + function WorkerScope() { + self.port.emit("foo"); + } + ); + + worker.port.on("foo", function () { + assert.pass("You can use events"); + // And terminate your test with helper.done: + helper.done(); + }); + +}); + +/* Disabled due to bug 1038432 +// Bug 714778: There was some issue around `toString` functions +// that ended up being shared between content scripts +exports["test Shared To String Proxies"] = createProxyTest("", function(helper) { + + let worker = helper.createWorker( + 'new ' + function ContentScriptScope() { + // We ensure that `toString` can't be modified so that nothing could + // leak to/from the document and between content scripts + // It only applies to JS proxies, there isn't any such issue with xrays. + //document.location.toString = function foo() {}; + document.location.toString.foo = "bar"; + assert("foo" in document.location.toString, "document.location.toString can be modified"); + assert(document.location.toString() == "data:text/html;charset=utf-8,", + "First document.location.toString()"); + self.postMessage("next"); + } + ); + worker.on("message", function () { + helper.createWorker( + 'new ' + function ContentScriptScope2() { + assert(!("foo" in document.location.toString), + "document.location.toString is different for each content script"); + assert(document.location.toString() == "data:text/html;charset=utf-8,", + "Second document.location.toString()"); + done(); + } + ); + }); +}); +*/ + +// Ensure that postMessage is working correctly across documents with an iframe +let html = '

+

+

+

+

+

+

+

+ +`; +exports["test predicate context"] = withTab(function*(assert) { + const test = function*(selector, expect) { + var isMatch = false; + test.return = (target) => { + return isMatch = expect(target); + } + assert.deepEqual((yield captureContextMenu(selector)), + isMatch ? menugroup(menuseparator(), + menuitem({label:"predicate"})) : + menugroup(), + isMatch ? `predicate item matches ${selector}` : + `predicate item doesn't match ${selector}`); + }; + test.predicate = target => test.return(target); + + yield* withItems({ + item: new Item({ + label: "predicate", + read: { + mediaType: new Readers.MediaType(), + link: new Readers.LinkURL(), + isPage: new Readers.isPage(), + isFrame: new Readers.isFrame(), + isEditable: new Readers.isEditable(), + tagName: new Readers.Query("tagName"), + appCodeName: new Readers.Query("ownerDocument.defaultView.navigator.appCodeName"), + width: new Readers.Attribute("width"), + src: new Readers.SrcURL(), + url: new Readers.PageURL(), + selection: new Readers.Selection() + }, + context: [Contexts.Predicate(test.predicate)] + }) + }, function*(items) { + yield* test("strong p", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: true, + isFrame: false, + isEditable: false, + tagName: "P", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "pagraph read test"); + return true; + }); + + yield* test("a span", target => { + assert.deepEqual(target, { + mediaType: null, + link: "./link", + isPage: false, + isFrame: false, + isEditable: false, + tagName: "SPAN", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "video tag test"); + return false; + }); + + yield* test("h3", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: true, + isFrame: false, + isEditable: false, + tagName: "H3", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "video tag test"); + return false; + }); + + yield select("h3"); + + yield* test("a span", target => { + assert.deepEqual(target, { + mediaType: null, + link: "./link", + isPage: false, + isFrame: false, + isEditable: false, + tagName: "SPAN", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: "hi", + }, "test selection with link"); + return true; + }); + + yield select(null); + + + yield* test("button", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "BUTTON", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test button"); + return true; + }); + + yield* test("canvas", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "CANVAS", + appCodeName: "Mozilla", + width: "50", + src: null, + url: predicateTestURL, + selection: null, + }, "test button"); + return true; + }); + + yield* test("img", target => { + assert.deepEqual(target, { + mediaType: "image", + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "IMG", + appCodeName: "Mozilla", + width: "50", + src: "./no.png", + url: predicateTestURL, + selection: null, + }, "test image"); + return true; + }); + + yield* test("code", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: true, + tagName: "CODE", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test content editable"); + return false; + }); + + yield* test("input[readonly=true]", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "INPUT", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test readonly input"); + return false; + }); + + yield* test("input[disabled=true]", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "INPUT", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test disabled input"); + return false; + }); + + yield select({target: "input#text", start: 0, end: 5 }); + + yield* test("input#text", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: true, + tagName: "INPUT", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: "test ", + }, "test editable input"); + return false; + }); + + yield select({target: "input#text", start:0, end: 0}); + + yield* test("input[type=submit]", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "INPUT", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test submit input"); + return false; + }); + + yield* test("input[type=radio]", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "INPUT", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test radio input"); + return false; + }); + + yield* test("input[type=checkbox]", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "INPUT", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test checkbox input"); + return false; + }); + + yield* test("input[type=foo]", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: true, + tagName: "INPUT", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test unrecognized input"); + return false; + }); + + yield* test("textarea", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: true, + tagName: "TEXTAREA", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test textarea"); + return false; + }); + + + yield* test("iframe", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: true, + isFrame: true, + isEditable: false, + tagName: "BODY", + appCodeName: "Mozilla", + width: null, + src: null, + url: `data:text/html,Bye`, + selection: null, + }, "test iframe"); + return true; + }); + + yield* test("select", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "SELECT", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test select"); + return true; + }); + + yield* test("menu", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "MENU", + appCodeName: "Mozilla", + width: null, + src: null, + url: predicateTestURL, + selection: null, + }, "test menu"); + return false; + }); + + yield* test("video", target => { + assert.deepEqual(target, { + mediaType: "video", + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "VIDEO", + appCodeName: "Mozilla", + width: "50", + src: null, + url: predicateTestURL, + selection: null, + }, "test video"); + return true; + }); + + yield* test("audio", target => { + assert.deepEqual(target, { + mediaType: "audio", + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "AUDIO", + appCodeName: "Mozilla", + width: "10", + src: null, + url: predicateTestURL, + selection: null, + }, "test audio"); + return true; + }); + + yield* test("object", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "OBJECT", + appCodeName: "Mozilla", + width: "10", + src: null, + url: predicateTestURL, + selection: null, + }, "test object"); + return true; + }); + + yield* test("embed", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "EMBED", + appCodeName: "Mozilla", + width: "10", + src: null, + url: predicateTestURL, + selection: null, + }, "test embed"); + return true; + }); + + yield* test("applet", target => { + assert.deepEqual(target, { + mediaType: null, + link: null, + isPage: false, + isFrame: false, + isEditable: false, + tagName: "APPLET", + appCodeName: "Mozilla", + width: "30", + src: null, + url: predicateTestURL, + selection: null, + }, "test applet"); + return false; + }); + + }); +}, predicateTestURL); + +exports["test extractor reader"] = withTab(function*(assert) { + const test = function*(selector, expect) { + var isMatch = false; + test.return = (target) => { + return isMatch = expect(target); + } + assert.deepEqual((yield captureContextMenu(selector)), + isMatch ? menugroup(menuseparator(), + menuitem({label:"extractor"})) : + menugroup(), + isMatch ? `predicate item matches ${selector}` : + `predicate item doesn't match ${selector}`); + }; + test.predicate = target => test.return(target); + + + yield* withItems({ + item: new Item({ + label: "extractor", + context: [Contexts.Predicate(test.predicate)], + read: { + tagName: Readers.Query("tagName"), + selector: Readers.Extractor(target => { + let node = target; + let path = []; + while (node) { + if (node.id) { + path.unshift(`#${node.id}`); + node = null; + } + else { + path.unshift(node.localName); + node = node.parentElement; + } + } + return path.join(" > "); + }) + } + }) + }, function*(_) { + yield* test("footer", target => { + assert.deepEqual(target, { + tagName: "FOOTER", + selector: "html > body > nav > footer" + }, "test footer"); + return false; + }); + + + }); +}, data` + + +
+
First title
+
+

First paragraph

+

Second paragraph

+
+
+
+
Second title
+
+

First paragraph

+

Second paragraph

+
+
+ +`); + +exports["test items overflow"] = withTab(function*(assert) { + yield* withItems({ + i1: new Item({label: "item-1"}), + i2: new Item({label: "item-2"}), + i3: new Item({label: "item-3"}), + i4: new Item({label: "item-4"}), + i5: new Item({label: "item-5"}), + i6: new Item({label: "item-6"}), + i7: new Item({label: "item-7"}), + i8: new Item({label: "item-8"}), + i9: new Item({label: "item-9"}), + i10: new Item({label: "item-10"}), + }, function*(_) { + assert.deepEqual((yield captureContextMenu("p")), + menugroup(menu({ + className: "sdk-context-menu-overflow-menu", + label: "Add-ons", + accesskey: "A", + }, menuitem({label: "item-1"}), + menuitem({label: "item-2"}), + menuitem({label: "item-3"}), + menuitem({label: "item-4"}), + menuitem({label: "item-5"}), + menuitem({label: "item-6"}), + menuitem({label: "item-7"}), + menuitem({label: "item-8"}), + menuitem({label: "item-9"}), + menuitem({label: "item-10"}))), + "context menu has an overflow"); + }); + + prefs.set("extensions.addon-sdk.context-menu.overflowThreshold", 3); + + yield* withItems({ + i1: new Item({label: "item-1"}), + i2: new Item({label: "item-2"}), + }, function*(_) { + assert.deepEqual((yield captureContextMenu("p")), + menugroup(menuseparator(), + menuitem({label: "item-1"}), + menuitem({label: "item-2"})), + "two items do not overflow"); + }); + + yield* withItems({ + one: new Item({label: "one"}), + two: new Item({label: "two"}), + three: new Item({label: "three"}) + }, function*(_) { + assert.deepEqual((yield captureContextMenu("p")), + menugroup(menu({className: "sdk-context-menu-overflow-menu", + label: "Add-ons", + accesskey: "A"}, + menuitem({label: "one"}), + menuitem({label: "two"}), + menuitem({label: "three"}))), + "three items overflow"); + }); + + prefs.reset("extensions.addon-sdk.context-menu.overflowThreshold"); + + yield* withItems({ + one: new Item({label: "one"}), + two: new Item({label: "two"}), + three: new Item({label: "three"}) + }, function*(_) { + assert.deepEqual((yield captureContextMenu("p")), + menugroup(menuseparator(), + menuitem({label: "one"}), + menuitem({label: "two"}), + menuitem({label: "three"})), + "three items no longer overflow"); + }); +}, data`

Hello

`); + + +exports["test context menus"] = withTab(function*(assert) { + const one = new Item({ + label: "one", + context: [Contexts.Selector("p")], + read: {tagName: Readers.Query("tagName")} + }); + + assert.deepEqual((yield captureContextMenu("p")), + menugroup(menuseparator(), + menuitem({label: "one"})), + "item is present"); + + const two = new Item({ + label: "two", + read: {tagName: Readers.Query("tagName")} + }); + + + assert.deepEqual((yield captureContextMenu("p")), + menugroup(menuseparator(), + menuitem({label: "one"}), + menuitem({label: "two"})), + "both items are present"); + + const groupLevel1 = new Menu({label: "Level 1"}, + [one]); + + assert.deepEqual((yield captureContextMenu("p")), + menugroup(menuseparator(), + menuitem({label: "two"}), + menu({label: "Level 1"}, + menuitem({label: "one"}))), + "first item moved to group"); + + assert.deepEqual((yield captureContextMenu("h1")), + menugroup(menuseparator(), + menuitem({label: "two"})), + "menu is hidden since only item does not match"); + + + const groupLevel2 = new Menu({label: "Level 2" }, [groupLevel1]); + + assert.deepEqual((yield captureContextMenu("p")), + menugroup(menuseparator(), + menuitem({label: "two"}), + menu({label: "Level 2"}, + menu({label: "Level 1"}, + menuitem({label: "one"})))), + "top level menu moved to submenu"); + + assert.deepEqual((yield captureContextMenu("h1")), + menugroup(menuseparator(), + menuitem({label: "two"})), + "menu is hidden since only item does not match"); + + + const contextGroup = new Menu({ + label: "H1 Group", + context: [Contexts.Selector("h1")] + }, [ + two, + new Separator(), + new Item({ label: "three" }) + ]); + + + assert.deepEqual((yield captureContextMenu("p")), + menugroup(menuseparator(), + menu({label: "Level 2"}, + menu({label: "Level 1"}, + menuitem({label: "one"})))), + "nested menu is rendered"); + + assert.deepEqual((yield captureContextMenu("h1")), + menugroup(menuseparator(), + menu({label: "H1 Group"}, + menuitem({label: "two"}), + menuseparator(), + menuitem({label: "three"}))), + "new contextual menu rendered"); + + yield* withItems({one, two, + groupLevel1, groupLevel2, contextGroup}, function*() { + + }); + + assert.deepEqual((yield captureContextMenu("p")), + menugroup(), + "everyhing matching p was desposed"); + + assert.deepEqual((yield captureContextMenu("h1")), + menugroup(), + "everyhing matching h1 was desposed"); + +}, data`

Title

Content

`); + +exports["test unloading"] = withTab(function*(assert) { + const { Loader } = require("sdk/test/loader"); + const loader = Loader(module); + + const {Item, Menu, Separator, Contexts, Readers } = loader.require("sdk/context-menu@2"); + + const item = new Item({label: "item"}); + const group = new Menu({label: "menu"}, + [new Separator(), + new Item({label: "sub-item"})]); + assert.deepEqual((yield captureContextMenu()), + menugroup(menuseparator(), + menuitem({label: "item"}), + menu({label: "menu"}, + menuseparator(), + menuitem({label: "sub-item"}))), + "all items rendered"); + + + loader.unload(); + + assert.deepEqual((yield captureContextMenu()), + menugroup(), + "all items disposed"); +}, data``); + +if (require("@loader/options").isNative) { + module.exports = { + "test skip on jpm": (assert) => assert.pass("skipping this file with jpm") + }; +} + +require("sdk/test").run(module.exports); diff --git a/addon-sdk/source/test/test-cuddlefish.js b/addon-sdk/source/test/test-cuddlefish.js index 2d8bb6102a3c..c92eaa624940 100644 --- a/addon-sdk/source/test/test-cuddlefish.js +++ b/addon-sdk/source/test/test-cuddlefish.js @@ -1,13 +1,44 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - 'use strict'; -const { Loader, Require, unload, override } = require('sdk/loader/cuddlefish'); -const packaging = require('@loader/options'); +const { Cc, Ci, Cu, CC, Cr, Cm, ChromeWorker, components } = require("chrome"); + +const packaging = require("@loader/options"); +const app = require('sdk/system/xul-app'); +const { resolve } = require; + +const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1']. + getService(Ci.mozIJSSubScriptLoader); +const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); + +function loadSandbox(uri) { + let proto = { + sandboxPrototype: { + loadSandbox: loadSandbox, + ChromeWorker: ChromeWorker + } + }; + let sandbox = Cu.Sandbox(systemPrincipal, proto); + // Create a fake commonjs environnement just to enable loading loader.js + // correctly + sandbox.exports = {}; + sandbox.module = { uri: uri, exports: sandbox.exports }; + sandbox.require = function (id) { + if (id !== "chrome") + throw new Error("Bootstrap sandbox `require` method isn't implemented."); + + return Object.freeze({ Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm, + CC: CC, components: components, + ChromeWorker: ChromeWorker }); + }; + scriptLoader.loadSubScript(uri, sandbox, 'UTF-8'); + return sandbox; +} exports['test loader'] = function(assert) { + let { Loader, Require, unload, override } = loadSandbox(resolve('sdk/loader/cuddlefish.js')).exports; var prints = []; function print(message) { prints.push(message); @@ -44,4 +75,4 @@ exports['test loader'] = function(assert) { 'loader.unload() must call listeners in LIFO order.'); }; -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-deprecate.js b/addon-sdk/source/test/test-deprecate.js index ce306b15b780..c1bd443c614f 100644 --- a/addon-sdk/source/test/test-deprecate.js +++ b/addon-sdk/source/test/test-deprecate.js @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -'use strict'; +"use strict"; const deprecate = require("sdk/util/deprecate"); const { LoaderWithHookedConsole } = require("sdk/test/loader"); @@ -86,6 +85,7 @@ exports.testDeprecateEvent = function(assert, done) { assert.equal(messages.length, 1, "only one error is dispatched"); emit(testObj, 'water'); }); + assert.equal(messages.length, 1, "only one error is dispatched"); assert.equal(messages[0].type, "error", "the console message is an error"); let msg = messages[0].msg; @@ -98,16 +98,16 @@ exports.testDeprecateEvent = function(assert, done) { emit(testObj, 'fire'); } -exports.testDeprecateSettingToggle = function (assert, done) { +exports.testDeprecateSettingToggle = function (assert) { let { loader, messages } = LoaderWithHookedConsole(module); let deprecate = loader.require("sdk/util/deprecate"); - + function fn () { deprecate.deprecateUsage("foo"); } set(PREFERENCE, false); fn(); assert.equal(messages.length, 0, 'no deprecation warnings'); - + set(PREFERENCE, true); fn(); assert.equal(messages.length, 1, 'deprecation warnings when toggled'); @@ -115,7 +115,6 @@ exports.testDeprecateSettingToggle = function (assert, done) { set(PREFERENCE, false); fn(); assert.equal(messages.length, 1, 'no new deprecation warnings'); - done(); }; exports.testDeprecateSetting = function (assert, done) { @@ -157,4 +156,5 @@ exports.testDeprecateSetting = function (assert, done) { emit(testObj, 'fire'); } -require("test").run(exports); + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-dev-panel.js b/addon-sdk/source/test/test-dev-panel.js index 5ef886fc3aa5..e80dced1bbef 100644 --- a/addon-sdk/source/test/test-dev-panel.js +++ b/addon-sdk/source/test/test-dev-panel.js @@ -100,7 +100,7 @@ exports["test Panel communication"] = test(function*(assert) { if (event.source === window) { var port = event.ports[0]; port.start(); - port.postMessage("ping");; + port.postMessage("ping"); port.onmessage = (event) => { if (event.data === "pong") { port.postMessage("bye"); @@ -301,7 +301,6 @@ exports["test createView panel"] = test(function*(assert) { } }); - const toolbox = yield openToolbox(MyPanel); const myPanel = yield getCurrentPanel(toolbox); @@ -317,6 +316,4 @@ exports["test createView panel"] = test(function*(assert) { yield closeToolbox(); }); - -require("test").run(exports); - +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-environment.js b/addon-sdk/source/test/test-environment.js index 8475d244d282..9fec6f83dd06 100644 --- a/addon-sdk/source/test/test-environment.js +++ b/addon-sdk/source/test/test-environment.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - 'use strict'; const { env } = require('sdk/system/environment'); @@ -47,4 +46,4 @@ exports['test unset'] = function(assert) { assert.equal('BLA4' in env, false, 'BLA4 env variable no longer exists' ); }; -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-event-utils.js b/addon-sdk/source/test/test-event-utils.js index 649c3a3b424c..1ec34fbe3368 100644 --- a/addon-sdk/source/test/test-event-utils.js +++ b/addon-sdk/source/test/test-event-utils.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - 'use strict'; const { on, emit } = require("sdk/event/core"); @@ -56,7 +55,7 @@ exports["test map events"] = function(assert) { exports["test map emits"] = $.emits(function(input, assert) { let output = map(input, inc); assert(output, [1, 2, 3], [2, 3, 4], "this is `output` & evens passed"); -});; +}); exports["test map reg once"] = $.registerOnce(function(input, assert) { assert(map(input, inc), [1, 2, 3], [2, 3, 4], @@ -279,4 +278,4 @@ exports['test stripListeners'] = function (assert) { function noop2 () {} }; -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-frame-utils.js b/addon-sdk/source/test/test-frame-utils.js index 402016d1ed35..501f93c87c9f 100644 --- a/addon-sdk/source/test/test-frame-utils.js +++ b/addon-sdk/source/test/test-frame-utils.js @@ -56,4 +56,4 @@ exports['test frame with js enabled'] = function(assert, done) { }); }; -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-framescript-manager.js b/addon-sdk/source/test/test-framescript-manager.js new file mode 100644 index 000000000000..442f71edaef1 --- /dev/null +++ b/addon-sdk/source/test/test-framescript-manager.js @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {loadModule} = require("framescript/manager"); +const {withTab, receiveMessage} = require("./util"); +const {getBrowserForTab} = require("sdk/tabs/utils"); + +exports.testLoadModule = withTab(function*(assert, tab) { + const {messageManager} = getBrowserForTab(tab); + + loadModule(messageManager, + require.resolve("./framescript-manager/frame-script"), + true, + "onInit"); + + const message = yield receiveMessage(messageManager, "framescript-manager/ready"); + + assert.deepEqual(message.data, {state: "ready"}, + "received ready message from the loaded module"); + + messageManager.sendAsyncMessage("framescript-manager/ping", {x: 1}); + + const pong = yield receiveMessage(messageManager, "framescript-manager/pong"); + + assert.deepEqual(pong.data, {x: 1}, + "received pong back"); +}, "data:text/html,

Message Manager

"); + + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-framescript-util.js b/addon-sdk/source/test/test-framescript-util.js new file mode 100644 index 000000000000..0a55bcbf6c0a --- /dev/null +++ b/addon-sdk/source/test/test-framescript-util.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {loadModule} = require("framescript/manager"); +const {withTab, receiveMessage} = require("./util"); +const {getBrowserForTab} = require("sdk/tabs/utils"); + +exports["test windowToMessageManager"] = withTab(function*(assert, tab) { + const {messageManager} = getBrowserForTab(tab); + + loadModule(messageManager, + require.resolve("./framescript-util/frame-script"), + true, + "onInit"); + + messageManager.sendAsyncMessage("framescript-util/window/request"); + + const response = yield receiveMessage(messageManager, + "framescript-util/window/response"); + + assert.deepEqual(response.data, {window: true}, + "got response"); +}, "data:text/html,

Window to Message Manager

"); + + +exports["test nodeToMessageManager"] = withTab(function*(assert, tab) { + const {messageManager} = getBrowserForTab(tab); + + loadModule(messageManager, + require.resolve("./framescript-util/frame-script"), + true, + "onInit"); + + messageManager.sendAsyncMessage("framescript-util/node/request", "h1"); + + const response = yield receiveMessage(messageManager, + "framescript-util/node/response"); + + assert.deepEqual(response.data, {node: true}, + "got response"); +}, "data:text/html,

Node to Message Manager

"); + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-functional.js b/addon-sdk/source/test/test-functional.js index 80a2e9bde5b6..02ae15fc6512 100644 --- a/addon-sdk/source/test/test-functional.js +++ b/addon-sdk/source/test/test-functional.js @@ -460,4 +460,4 @@ exports["test throttle"] = (assert, done) => { new Array(11).join(0).split("").forEach(fn); }; -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-globals.js b/addon-sdk/source/test/test-globals.js index ba38ed7fb7c6..bc336436773b 100644 --- a/addon-sdk/source/test/test-globals.js +++ b/addon-sdk/source/test/test-globals.js @@ -27,4 +27,4 @@ exports.testComponent = function (assert) { }, /`Components` is not available/, 'using `Components` throws'); }; -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-heritage.js b/addon-sdk/source/test/test-heritage.js index f94861d7302e..e087f3e4dc86 100644 --- a/addon-sdk/source/test/test-heritage.js +++ b/addon-sdk/source/test/test-heritage.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; const { Class, extend, mix, obscure } = require('sdk/core/heritage'); @@ -299,4 +298,4 @@ exports['test composition with objects'] = function(assert) { assert.equal(f.e, 5, 'implements e'); }; -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-hidden-frame.js b/addon-sdk/source/test/test-hidden-frame.js index 3a375b9bfafb..945c2413f415 100644 --- a/addon-sdk/source/test/test-hidden-frame.js +++ b/addon-sdk/source/test/test-hidden-frame.js @@ -68,4 +68,4 @@ exports["test unload detaches panels"] = function(assert, done) { } }; -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-host-events.js b/addon-sdk/source/test/test-host-events.js index 72e03f396bdd..1a67aab2ad34 100644 --- a/addon-sdk/source/test/test-host-events.js +++ b/addon-sdk/source/test/test-host-events.js @@ -96,4 +96,4 @@ exports.testSerialization = function (assert, done) { } }; -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-hotkeys.js b/addon-sdk/source/test/test-hotkeys.js index 77fe9a45de0b..10b487cb50d4 100644 --- a/addon-sdk/source/test/test-hotkeys.js +++ b/addon-sdk/source/test/test-hotkeys.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; const { Hotkey } = require("sdk/hotkeys"); @@ -139,7 +138,7 @@ exports["test no exception on unmodified keypress"] = function(assert) { exports["test hotkey: automatic destroy"] = function(assert, done) { // Hacky way to be able to create unloadable modules via makeSandboxedLoader. let loader = Loader(module); - + var called = false; var element = loader.require("sdk/deprecated/window-utils").activeBrowserWindow.document.documentElement; var hotkey = loader.require("sdk/hotkeys").Hotkey({ @@ -148,17 +147,17 @@ exports["test hotkey: automatic destroy"] = function(assert, done) { called = true; } }); - + // Unload the module so that previous hotkey is automatically destroyed loader.unload(); - + // Ensure that the hotkey is really destroyed keyDown(element, "accel-shift-x"); - + timer.setTimeout(function () { assert.ok(!called, "Hotkey is destroyed and not called."); done(); }, 0); }; -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-indexed-db.js b/addon-sdk/source/test/test-indexed-db.js index 9650bcbac44f..20408f9b067a 100644 --- a/addon-sdk/source/test/test-indexed-db.js +++ b/addon-sdk/source/test/test-indexed-db.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; let xulApp = require("sdk/system/xul-app"); @@ -191,4 +190,4 @@ function testRead(assert, done) { } } -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-keyboard-observer.js b/addon-sdk/source/test/test-keyboard-observer.js index 091ec73c5b30..18f32eab30ef 100644 --- a/addon-sdk/source/test/test-keyboard-observer.js +++ b/addon-sdk/source/test/test-keyboard-observer.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; const { keyPress } = require("sdk/dom/events/keys"); @@ -34,4 +33,4 @@ exports["test unload keyboard observer"] = function(assert, done) { }, 0); }; -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-keyboard-utils.js b/addon-sdk/source/test/test-keyboard-utils.js index 19981dec3d68..00fc841eeb13 100644 --- a/addon-sdk/source/test/test-keyboard-utils.js +++ b/addon-sdk/source/test/test-keyboard-utils.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; const utils = require("sdk/keyboard/utils"); @@ -14,7 +13,7 @@ exports["test toString"] = function(assert) { key: "B", modifiers: [ "Shift", "Ctrl" ] }), "Shift-Ctrl-B", "toString does not normalizes JSON"); - + assert.equal(utils.toString({ key: "C", modifiers: [], @@ -26,7 +25,7 @@ exports["test toString"] = function(assert) { method: { value: function() {} } })), "alt-d", "Works with non-json objects"); - assert.equal(utils.toString({ + assert.equal(utils.toString({ modifiers: [ "shift", "alt" ] }), "shift-alt-", "works with only modifiers"); }; @@ -59,4 +58,4 @@ exports["test normalize"] = function assert(assert) { }, "throws if contains more then on non-modifier key"); }; -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-l10n-locale.js b/addon-sdk/source/test/test-l10n-locale.js index 87be1ca48ce3..564abbf1be6f 100644 --- a/addon-sdk/source/test/test-l10n-locale.js +++ b/addon-sdk/source/test/test-l10n-locale.js @@ -79,6 +79,32 @@ exports.testPreferedLocalizedLocale = function(assert) { prefs.reset(PREF_ACCEPT_LANGUAGES); } +// On Linux the PREF_ACCEPT_LANGUAGES pref can be a localized pref. +exports.testPreferedContentLocale = function(assert) { + prefs.set(PREF_MATCH_OS_LOCALE, false); + let noLocale = "", + bundleURL = "chrome://global/locale/intl.properties"; + prefs.set(PREF_SELECTED_LOCALE, noLocale); + prefs.setLocalized(PREF_ACCEPT_LANGUAGES, bundleURL); + + // Read the expected locale values from the property file + let expectedLocaleList = BundleService.createBundle(bundleURL). + GetStringFromName(PREF_ACCEPT_LANGUAGES). + split(","). + map(locale => locale.trim().toLowerCase()); + + // Add default "en-us" fallback if the main language is not already en-us + if (expectedLocaleList.indexOf("en-us") == -1) + expectedLocaleList.push("en-us"); + + assertPrefered(assert, expectedLocaleList, "test localized content locale pref value"); + + // Reset what we have changed + prefs.reset(PREF_MATCH_OS_LOCALE); + prefs.reset(PREF_SELECTED_LOCALE); + prefs.reset(PREF_ACCEPT_LANGUAGES); +} + exports.testPreferedOsLocale = function(assert) { prefs.set(PREF_MATCH_OS_LOCALE, true); prefs.set(PREF_SELECTED_LOCALE, ""); diff --git a/addon-sdk/source/test/test-type.js b/addon-sdk/source/test/test-lang-type.js similarity index 63% rename from addon-sdk/source/test/test-type.js rename to addon-sdk/source/test/test-lang-type.js index a474acbaf298..dd33c9a48f07 100644 --- a/addon-sdk/source/test/test-type.js +++ b/addon-sdk/source/test/test-lang-type.js @@ -1,36 +1,46 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict" var utils = require("sdk/lang/type"); exports["test function"] = function (assert) { - assert.ok(utils.isFunction(function(){}), "value is function"); - assert.ok(utils.isFunction(Object), "Object is function"); - assert.ok(utils.isFunction(new Function("")), "Genertaed value is function"); - assert.ok(!utils.isFunction({}), "object is not a function"); - assert.ok(!utils.isFunction(4), "number is not a function"); + assert.equal(utils.isFunction(function(){}), true, "value is a function"); + assert.equal(utils.isFunction(Object), true, "Object is a function"); + assert.equal(utils.isFunction(new Function("")), true, "Genertaed value is a function"); + assert.equal(utils.isFunction({}), false, "object is not a function"); + assert.equal(utils.isFunction(4), false, "number is not a function"); }; exports["test atoms"] = function (assert) { - assert.ok(utils.isPrimitive(2), "number is primitive"); - assert.ok(utils.isPrimitive(NaN), "`NaN` is primitve"); - assert.ok(utils.isPrimitive(undefined), "`undefined` is primitive"); - assert.ok(utils.isPrimitive(null), "`null` is primitive"); - assert.ok(utils.isPrimitive(Infinity), "`Infinity` is primitive"); - assert.ok(utils.isPrimitive("foo"), "strings are primitive"); + assert.equal(utils.isPrimitive(2), true, "number is a primitive"); + assert.equal(utils.isPrimitive(NaN), true, "`NaN` is a primitve"); + assert.equal(utils.isPrimitive(undefined), true, "`undefined` is a primitive"); + assert.equal(utils.isPrimitive(null), true, "`null` is a primitive"); + assert.equal(utils.isPrimitive(Infinity), true, "`Infinity` is a primitive"); + assert.equal(utils.isPrimitive("foo"), true, "strings are a primitive"); assert.ok(utils.isPrimitive(true) && utils.isPrimitive(false), "booleans are primitive"); }; exports["test object"] = function (assert) { - assert.ok(utils.isObject({}), "`{}` is object"); + assert.equal(utils.isObject({}), true, "`{}` is an object"); assert.ok(!utils.isObject(null), "`null` is not an object"); assert.ok(!utils.isObject(Object), "functions is not an object"); }; +exports["test generator"] = function (assert) { + assert.equal(utils.isGenerator(function*(){}), true, "`function*(){}` is a generator"); + assert.equal(utils.isGenerator(function(){}), false, "`function(){}` is not a generator"); + assert.equal(utils.isGenerator(() => {}), false, "`() => {}` is not a generator"); + assert.equal(utils.isGenerator({}), false, "`{}` is not a generator"); + assert.equal(utils.isGenerator(1), false, "`1` is not a generator"); + assert.equal(utils.isGenerator([]), false, "`[]` is not a generator"); + assert.equal(utils.isGenerator(null), false, "`null` is not a generator"); + assert.equal(utils.isGenerator(undefined), false, "`undefined` is not a generator"); +}; + exports["test flat objects"] = function (assert) { assert.ok(utils.isFlat({}), "`{}` is a flat object"); assert.ok(!utils.isFlat([]), "`[]` is not a flat object"); @@ -89,4 +99,4 @@ exports["test json"] = function (assert) { "json must be direct descendant of `Object.prototype`"); }; -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-libxul.js b/addon-sdk/source/test/test-libxul.js index eb6631ac24e1..7a11a69cb648 100644 --- a/addon-sdk/source/test/test-libxul.js +++ b/addon-sdk/source/test/test-libxul.js @@ -15,4 +15,4 @@ exports.test = function(assert) { assert.ok(lib != null, "linked to libxul successfully"); }; -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-light-traits.js b/addon-sdk/source/test/test-light-traits.js index 7c5ca42d3e16..9d9c010e9f68 100644 --- a/addon-sdk/source/test/test-light-traits.js +++ b/addon-sdk/source/test/test-light-traits.js @@ -1,11 +1,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; exports["test traits from objects"] = require("./traits/object-tests"); exports["test traits from descriptors"] = require("./traits/descriptor-tests"); exports["test inheritance"] = require("./traits/inheritance-tests"); -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-loader.js b/addon-sdk/source/test/test-loader.js index 44e17270dbfd..45b96fe8ae1c 100644 --- a/addon-sdk/source/test/test-loader.js +++ b/addon-sdk/source/test/test-loader.js @@ -1,16 +1,17 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - 'use strict'; let { - Loader, main, unload, parseStack, generateMap, resolve, join + Loader, main, unload, parseStack, generateMap, resolve, join, + Require, Module } = require('toolkit/loader'); let { readURI } = require('sdk/net/url'); -let root = module.uri.substr(0, module.uri.lastIndexOf('/')) +let root = module.uri.substr(0, module.uri.lastIndexOf('/')); +const app = require('sdk/system/xul-app'); // The following adds Debugger constructor to the global namespace. const { Cu } = require('chrome'); @@ -331,7 +332,7 @@ exports['test console global by default'] = function (assert) { let uri = root + '/fixtures/loader/globals/'; let loader = Loader({ paths: { '': uri }}); let program = main(loader, 'main'); - + assert.ok(typeof program.console === 'object', 'global `console` exists'); assert.ok(typeof program.console.log === 'function', 'global `console.log` exists'); @@ -374,4 +375,135 @@ exports["test require#resolve"] = function(assert) { assert.equal(foundRoot + "toolkit/loader.js", require.resolve("toolkit/loader"), "correct resolution of sdk module"); }; -require('test').run(exports); +const modulesURI = require.resolve("toolkit/loader").replace("toolkit/loader.js", ""); +exports["test loading a loader"] = function(assert) { + const loader = Loader({ paths: { "": modulesURI } }); + + const require = Require(loader, module); + + const requiredLoader = require("toolkit/loader"); + + assert.equal(requiredLoader.Loader, Loader, + "got the same Loader instance"); + + const jsmLoader = Cu.import(require.resolve("toolkit/loader"), {}).Loader; + + assert.equal(jsmLoader.Loader, requiredLoader.Loader, + "loading loader via jsm returns same loader"); + + unload(loader); +}; + +exports['test loader on unsupported modules with checkCompatibility true'] = function(assert) { + let loader = Loader({ + paths: { '': root + "/" }, + checkCompatibility: true + }); + let require = Require(loader, module); + + assert.throws(() => { + if (!app.is('Firefox')) { + require('fixtures/loader/unsupported/firefox'); + } + else { + require('fixtures/loader/unsupported/fennec'); + } + }, /^Unsupported Application/, "throws Unsupported Application"); + + unload(loader); +}; + +exports['test loader on unsupported modules with checkCompatibility false'] = function(assert) { + let loader = Loader({ + paths: { '': root + "/" }, + checkCompatibility: false + }); + let require = Require(loader, module); + + try { + if (!app.is('Firefox')) { + require('fixtures/loader/unsupported/firefox'); + } + else { + require('fixtures/loader/unsupported/fennec'); + } + assert.pass("loaded unsupported module without an error"); + } + catch(e) { + assert.fail(e); + } + + unload(loader); +}; + +exports['test loader on unsupported modules with checkCompatibility default'] = function(assert) { + let loader = Loader({ paths: { '': root + "/" } }); + let require = Require(loader, module); + + try { + if (!app.is('Firefox')) { + require('fixtures/loader/unsupported/firefox'); + } + else { + require('fixtures/loader/unsupported/fennec'); + } + assert.pass("loaded unsupported module without an error"); + } + catch(e) { + assert.fail(e); + } + + unload(loader); +}; + +exports["test Cu.import of toolkit/loader"] = (assert) => { + const toolkitLoaderURI = require.resolve("toolkit/loader"); + const loaderModule = Cu.import(toolkitLoaderURI).Loader; + const { Loader, Require, Main } = loaderModule; + const version = "0.1.0"; + const id = `fxos_${version.replace(".", "_")}_simulator@mozilla.org`; + const uri = `resource://${encodeURIComponent(id.replace("@", "at"))}/`; + + const loader = Loader({ + paths: { + "./": uri + "lib/", + // Can't just put `resource://gre/modules/commonjs/` as it + // won't take module overriding into account. + "": toolkitLoaderURI.replace("toolkit/loader.js", "") + }, + globals: { + console: console + }, + modules: { + "toolkit/loader": loaderModule, + addon: { + id: "simulator", + version: "0.1", + uri: uri + } + } + }); + + let require_ = Require(loader, { id: "./addon" }); + assert.equal(typeof(loaderModule), + typeof(require_("toolkit/loader")), + "module returned is whatever was mapped to it"); +}; + +exports["test Cu.import in b2g style"] = (assert) => { + const {FakeCu} = require("./loader/b2g"); + const toolkitLoaderURI = require.resolve("toolkit/loader"); + const b2g = new FakeCu(); + + const exported = {}; + const loader = b2g.import(toolkitLoaderURI, exported); + + assert.equal(typeof(exported.Loader), + "function", + "loader is a function"); + assert.equal(typeof(exported.Loader.Loader), + "function", + "Loader.Loader is a funciton"); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-module.js b/addon-sdk/source/test/test-module.js index 6c9c6c2b7053..1f9979e4b6e0 100644 --- a/addon-sdk/source/test/test-module.js +++ b/addon-sdk/source/test/test-module.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; /** Disabled because of Bug 672199 @@ -34,4 +33,4 @@ exports["test can't override exported property"] = function(assert) { assert.equal(hotkeys.Hotkey, Hotkey, "exports can't be overriden"); }; -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-namespace.js b/addon-sdk/source/test/test-namespace.js index 46f194a3aa73..636682e9e40b 100644 --- a/addon-sdk/source/test/test-namespace.js +++ b/addon-sdk/source/test/test-namespace.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; const { ns } = require("sdk/core/namespace"); @@ -118,4 +117,4 @@ exports["test ns inheritance"] = function(assert) { "descendants properties are inherited"); }; -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-native-loader.js b/addon-sdk/source/test/test-native-loader.js index 081daa0bc41f..c77894dd4730 100644 --- a/addon-sdk/source/test/test-native-loader.js +++ b/addon-sdk/source/test/test-native-loader.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - 'use strict'; let { @@ -259,4 +258,4 @@ function loadAddon (uri, map) { }).then(null, console.error); } -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-native-options.js b/addon-sdk/source/test/test-native-options.js index 01c6ecb84a9c..b619b4f24de0 100644 --- a/addon-sdk/source/test/test-native-options.js +++ b/addon-sdk/source/test/test-native-options.js @@ -109,7 +109,7 @@ exports.testSimplePrefs = function(assert) { let { preferences } = packageJSON('simple-prefs'); let branch = prefsrv.getDefaultBranch('extensions.' + preferencesBranch); - function assertPref(setting, name, type, title) { + function assertPref(setting, name, type, title, description = null) { assert.equal(setting.getAttribute('data-jetpack-id'), id, "setting 'data-jetpack-id' attribute correct"); assert.equal(setting.getAttribute('pref'), 'extensions.' + id + '.' + name, @@ -120,6 +120,14 @@ exports.testSimplePrefs = function(assert) { "setting 'type' attribute correct"); assert.equal(setting.getAttribute('title'), title, "setting 'title' attribute correct"); + if (description) { + assert.equal(setting.getAttribute('desc'), description, + "setting 'desc' attribute correct"); + } + else { + assert.ok(!setting.hasAttribute('desc'), + "setting 'desc' attribute is not present"); + } } function assertOption(option, value, label) { @@ -131,7 +139,7 @@ exports.testSimplePrefs = function(assert) { injectOptions(preferences, preferencesBranch, document, parent); assert.equal(parent.children.length, 8, "Eight setting elements injected"); - assertPref(parent.children[0], 'test', 'bool', 't\u00EBst'); + assertPref(parent.children[0], 'test', 'bool', 't\u00EBst', 'descr\u00EFpti\u00F6n'); assertPref(parent.children[1], 'test2', 'string', 't\u00EBst'); assertPref(parent.children[2], 'test3', 'menulist', '">"; - - let pageMod = new PageMod({ - include: privateUri, - onAttach: function(worker) { - assert.equal(worker.tab.url, - privateUri, - "page-mod should attach"); - assert.equal(isPrivateBrowsingSupported, - false, - "private browsing is not supported"); - assert.ok(isPrivate(worker), - "The worker is really non-private"); - assert.ok(isPrivate(worker.tab), - "The document is really non-private"); - pageMod.destroy(); - - worker.tab.close(function() { - pb.once('stop', function() { - assert.pass('global pb stop'); - done(); - }); - pb.deactivate(); - }); - } - }); - - let page1; - pb.once('start', function() { - assert.pass('global pb start'); - tabs.open({ url: privateUri }); - }); - pb.activate(); -} - // Bug 699450: Calling worker.tab.close() should not lead to exception exports.testWorkerTabClose = function(assert, done) { let callbackDone; @@ -1677,33 +1637,29 @@ exports.testConsole = function(assert, done) { let tab = openTab(getMostRecentBrowserWindow(), TEST_URL); } -exports.testSyntaxErrorInContentScript = function(assert, done) { +exports.testSyntaxErrorInContentScript = function *(assert) { const url = "data:text/html;charset=utf-8,testSyntaxErrorInContentScript"; - let hitError = null; - let attached = false; + const loader = createLoader(); + const { PageMod } = loader.require("sdk/page-mod"); + let attached = defer(); + let errored = defer(); - testPageMod(assert, done, url, [{ - include: url, - contentScript: 'console.log(23', + let mod = PageMod({ + include: url, + contentScript: 'console.log(23', + onAttach: attached.resolve, + onError: errored.resolve + }); + openNewTab(url); - onAttach: function() { - attached = true; - }, + yield attached.promise; + let hitError = yield errored.promise; - onError: function(e) { - hitError = e; - } - }], + assert.notStrictEqual(hitError, null, "The syntax error was reported."); + assert.equal(hitError.name, "SyntaxError", "The error thrown should be a SyntaxError"); - function(win, done) { - assert.ok(attached, "The worker was attached."); - assert.notStrictEqual(hitError, null, "The syntax error was reported."); - if (hitError) - assert.equal(hitError.name, "SyntaxError", "The error thrown should be a SyntaxError"); - done(); - }, - 300 - ); + loader.unload(); + yield cleanUI(); }; require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-page-worker.js b/addon-sdk/source/test/test-page-worker.js index 63b42f55eda8..5db7dba30561 100644 --- a/addon-sdk/source/test/test-page-worker.js +++ b/addon-sdk/source/test/test-page-worker.js @@ -36,9 +36,15 @@ exports.testWrappedDOM = function(assert, done) { let page = Page({ allow: { script: true }, contentURL: "data:text/html;charset=utf-8,", - contentScript: "window.addEventListener('load', function () " + - "self.postMessage([typeof(document.getElementById), " + - "typeof(window.scrollTo)]), true)", + contentScript: 'new ' + function() { + function send() { + self.postMessage([typeof(document.getElementById), typeof(window.scrollTo)]); + } + if (document.readyState !== 'complete') + window.addEventListener('load', send, true) + else + send(); + }, onMessage: function (message) { assert.equal(message[0], "function", @@ -264,8 +270,8 @@ exports.testLoadContentPage = function(assert, done) { return done(); assert[msg].apply(assert, message); }, - contentURL: fixtures.url("test-page-worker.html"), - contentScriptFile: fixtures.url("test-page-worker.js"), + contentURL: fixtures.url("addon-sdk/data/test-page-worker.html"), + contentScriptFile: fixtures.url("addon-sdk/data/test-page-worker.js"), contentScriptWhen: "ready" }); } @@ -274,13 +280,10 @@ exports.testLoadContentPageRelativePath = function(assert, done) { const self = require("sdk/self"); const { merge } = require("sdk/util/object"); - let loader = Loader(module, null, null, { - modules: { - "sdk/self": merge({}, self, { - data: merge({}, self.data, fixtures) - }) - } - }); + const options = merge({}, require('@loader/options'), + { prefixURI: require('./fixtures').url() }); + + let loader = Loader(module, null, options); let page = loader.require("sdk/page-worker").Page({ onMessage: function(message) { @@ -526,4 +529,4 @@ function isDestroyed(page) { return false; } -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-panel.js b/addon-sdk/source/test/test-panel.js index 473aa1b5a207..602d8f547815 100644 --- a/addon-sdk/source/test/test-panel.js +++ b/addon-sdk/source/test/test-panel.js @@ -19,9 +19,9 @@ const { isPrivate } = require('sdk/private-browsing'); const { isWindowPBSupported } = require('sdk/private-browsing/utils'); const { defer, all } = require('sdk/core/promise'); const { getMostRecentBrowserWindow } = require('sdk/window/utils'); -const { getWindow } = require('sdk/panel/window'); const { URL } = require('sdk/url'); const { wait } = require('./event/helpers'); +const packaging = require('@loader/options'); const fixtures = require('./fixtures') @@ -587,6 +587,10 @@ exports["test Show Then Destroy"] = makeEventOrderTest({ } }); + +// TODO: Re-enable and fix this intermittent test +// See Bug 1111695 https://bugzilla.mozilla.org/show_bug.cgi?id=1111695 +/* exports["test Show Then Hide Then Destroy"] = makeEventOrderTest({ test: function(assert, done, expect, panel) { panel.show(); @@ -594,28 +598,28 @@ exports["test Show Then Hide Then Destroy"] = makeEventOrderTest({ then('hide', function() { panel.destroy(); done(); }); } }); +*/ exports["test Content URL Option"] = function(assert) { const { Panel } = require('sdk/panel'); const URL_STRING = "about:buildconfig"; const HTML_CONTENT = "Test

This is a test.

"; - - let (panel = Panel({ contentURL: URL_STRING })) { - assert.pass("contentURL accepts a string URL."); - assert.equal(panel.contentURL, URL_STRING, - "contentURL is the string to which it was set."); - } - let dataURL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML_CONTENT); - let (panel = Panel({ contentURL: dataURL })) { - assert.pass("contentURL accepts a data: URL."); - } - let (panel = Panel({})) { - assert.ok(panel.contentURL == null, - "contentURL is undefined."); - } + let panel = Panel({ contentURL: URL_STRING }); + assert.pass("contentURL accepts a string URL."); + assert.equal(panel.contentURL, URL_STRING, + "contentURL is the string to which it was set."); + panel.destroy(); + + panel = Panel({ contentURL: dataURL }); + assert.pass("contentURL accepts a data: URL."); + panel.destroy(); + + panel = Panel({}); + assert.ok(panel.contentURL == null, "contentURL is undefined."); + panel.destroy(); assert.throws(function () Panel({ contentURL: "foo" }), /The `contentURL` option must be a valid URL./, @@ -1331,20 +1335,10 @@ exports["test panel load doesn't show"] = function*(assert) { loader.unload(); } -if (isWindowPBSupported) { - exports.testGetWindow = function(assert, done) { - let activeWindow = getMostRecentBrowserWindow(); - open(null, { features: { - toolbar: true, - chrome: true, - private: true - } }).then(window => { - assert.ok(isPrivate(window), 'window is private'); - assert.equal(getWindow(window.gBrowser), null, 'private window elements returns null'); - assert.equal(getWindow(activeWindow.gBrowser), activeWindow, 'non-private window elements returns window'); - return window; - }).then(close).then(done).then(null, assert.fail); - } +if (packaging.isNative) { + module.exports = { + "test skip on jpm": (assert) => assert.pass("skipping this file with jpm") + }; } require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-passwords-utils.js b/addon-sdk/source/test/test-passwords-utils.js index 37730908967e..72b25c78bf35 100644 --- a/addon-sdk/source/test/test-passwords-utils.js +++ b/addon-sdk/source/test/test-passwords-utils.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; const { store, search, remove } = require("sdk/passwords/utils"); @@ -139,4 +138,4 @@ exports["test site authentication credentials"] = function(assert) { assert.ok(!search(options).length, "remove worked"); }; -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-passwords.js b/addon-sdk/source/test/test-passwords.js index 04e2181fd64c..dc33cfbb7877 100644 --- a/addon-sdk/source/test/test-passwords.js +++ b/addon-sdk/source/test/test-passwords.js @@ -277,4 +277,4 @@ exports["test site authentication credentials"] = function(assert, done) { }); }; -require("test").run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-path.js b/addon-sdk/source/test/test-path.js index f0b9e06cb0a2..7211def46c7f 100644 --- a/addon-sdk/source/test/test-path.js +++ b/addon-sdk/source/test/test-path.js @@ -1,431 +1,6 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; - -// Adapted version of: -// https://github.com/joyent/node/blob/v0.9.1/test/simple/test-path.js - -exports['test path'] = function(assert) { - -var system = require('sdk/system'); -var path = require('sdk/fs/path'); - -// Shim process global from node. -var process = Object.create(require('sdk/system')); -process.cwd = process.pathFor.bind(process, 'CurProcD'); - -var isWindows = require('sdk/system').platform.indexOf('win') === 0; - -assert.equal(path.basename(''), ''); -assert.equal(path.basename('/dir/basename.ext'), 'basename.ext'); -assert.equal(path.basename('/basename.ext'), 'basename.ext'); -assert.equal(path.basename('basename.ext'), 'basename.ext'); -assert.equal(path.basename('basename.ext/'), 'basename.ext'); -assert.equal(path.basename('basename.ext//'), 'basename.ext'); - -if (isWindows) { - // On Windows a backslash acts as a path separator. - assert.equal(path.basename('\\dir\\basename.ext'), 'basename.ext'); - assert.equal(path.basename('\\basename.ext'), 'basename.ext'); - assert.equal(path.basename('basename.ext'), 'basename.ext'); - assert.equal(path.basename('basename.ext\\'), 'basename.ext'); - assert.equal(path.basename('basename.ext\\\\'), 'basename.ext'); - -} else { - // On unix a backslash is just treated as any other character. - assert.equal(path.basename('\\dir\\basename.ext'), '\\dir\\basename.ext'); - assert.equal(path.basename('\\basename.ext'), '\\basename.ext'); - assert.equal(path.basename('basename.ext'), 'basename.ext'); - assert.equal(path.basename('basename.ext\\'), 'basename.ext\\'); - assert.equal(path.basename('basename.ext\\\\'), 'basename.ext\\\\'); -} - -// POSIX filenames may include control characters -// c.f. http://www.dwheeler.com/essays/fixing-unix-linux-filenames.html -if (!isWindows) { - var controlCharFilename = 'Icon' + String.fromCharCode(13); - assert.equal(path.basename('/a/b/' + controlCharFilename), - controlCharFilename); -} - -assert.equal(path.dirname('/a/b/'), '/a'); -assert.equal(path.dirname('/a/b'), '/a'); -assert.equal(path.dirname('/a'), '/'); -assert.equal(path.dirname(''), '.'); -assert.equal(path.dirname('/'), '/'); -assert.equal(path.dirname('////'), '/'); - -if (isWindows) { - assert.equal(path.dirname('c:\\'), 'c:\\'); - assert.equal(path.dirname('c:\\foo'), 'c:\\'); - assert.equal(path.dirname('c:\\foo\\'), 'c:\\'); - assert.equal(path.dirname('c:\\foo\\bar'), 'c:\\foo'); - assert.equal(path.dirname('c:\\foo\\bar\\'), 'c:\\foo'); - assert.equal(path.dirname('c:\\foo\\bar\\baz'), 'c:\\foo\\bar'); - assert.equal(path.dirname('\\'), '\\'); - assert.equal(path.dirname('\\foo'), '\\'); - assert.equal(path.dirname('\\foo\\'), '\\'); - assert.equal(path.dirname('\\foo\\bar'), '\\foo'); - assert.equal(path.dirname('\\foo\\bar\\'), '\\foo'); - assert.equal(path.dirname('\\foo\\bar\\baz'), '\\foo\\bar'); - assert.equal(path.dirname('c:'), 'c:'); - assert.equal(path.dirname('c:foo'), 'c:'); - assert.equal(path.dirname('c:foo\\'), 'c:'); - assert.equal(path.dirname('c:foo\\bar'), 'c:foo'); - assert.equal(path.dirname('c:foo\\bar\\'), 'c:foo'); - assert.equal(path.dirname('c:foo\\bar\\baz'), 'c:foo\\bar'); - assert.equal(path.dirname('\\\\unc\\share'), '\\\\unc\\share'); - assert.equal(path.dirname('\\\\unc\\share\\foo'), '\\\\unc\\share\\'); - assert.equal(path.dirname('\\\\unc\\share\\foo\\'), '\\\\unc\\share\\'); - assert.equal(path.dirname('\\\\unc\\share\\foo\\bar'), - '\\\\unc\\share\\foo'); - assert.equal(path.dirname('\\\\unc\\share\\foo\\bar\\'), - '\\\\unc\\share\\foo'); - assert.equal(path.dirname('\\\\unc\\share\\foo\\bar\\baz'), - '\\\\unc\\share\\foo\\bar'); -} - - -assert.equal(path.extname(''), ''); -assert.equal(path.extname('/path/to/file'), ''); -assert.equal(path.extname('/path/to/file.ext'), '.ext'); -assert.equal(path.extname('/path.to/file.ext'), '.ext'); -assert.equal(path.extname('/path.to/file'), ''); -assert.equal(path.extname('/path.to/.file'), ''); -assert.equal(path.extname('/path.to/.file.ext'), '.ext'); -assert.equal(path.extname('/path/to/f.ext'), '.ext'); -assert.equal(path.extname('/path/to/..ext'), '.ext'); -assert.equal(path.extname('file'), ''); -assert.equal(path.extname('file.ext'), '.ext'); -assert.equal(path.extname('.file'), ''); -assert.equal(path.extname('.file.ext'), '.ext'); -assert.equal(path.extname('/file'), ''); -assert.equal(path.extname('/file.ext'), '.ext'); -assert.equal(path.extname('/.file'), ''); -assert.equal(path.extname('/.file.ext'), '.ext'); -assert.equal(path.extname('.path/file.ext'), '.ext'); -assert.equal(path.extname('file.ext.ext'), '.ext'); -assert.equal(path.extname('file.'), '.'); -assert.equal(path.extname('.'), ''); -assert.equal(path.extname('./'), ''); -assert.equal(path.extname('.file.ext'), '.ext'); -assert.equal(path.extname('.file'), ''); -assert.equal(path.extname('.file.'), '.'); -assert.equal(path.extname('.file..'), '.'); -assert.equal(path.extname('..'), ''); -assert.equal(path.extname('../'), ''); -assert.equal(path.extname('..file.ext'), '.ext'); -assert.equal(path.extname('..file'), '.file'); -assert.equal(path.extname('..file.'), '.'); -assert.equal(path.extname('..file..'), '.'); -assert.equal(path.extname('...'), '.'); -assert.equal(path.extname('...ext'), '.ext'); -assert.equal(path.extname('....'), '.'); -assert.equal(path.extname('file.ext/'), '.ext'); -assert.equal(path.extname('file.ext//'), '.ext'); -assert.equal(path.extname('file/'), ''); -assert.equal(path.extname('file//'), ''); -assert.equal(path.extname('file./'), '.'); -assert.equal(path.extname('file.//'), '.'); - -if (isWindows) { - // On windows, backspace is a path separator. - assert.equal(path.extname('.\\'), ''); - assert.equal(path.extname('..\\'), ''); - assert.equal(path.extname('file.ext\\'), '.ext'); - assert.equal(path.extname('file.ext\\\\'), '.ext'); - assert.equal(path.extname('file\\'), ''); - assert.equal(path.extname('file\\\\'), ''); - assert.equal(path.extname('file.\\'), '.'); - assert.equal(path.extname('file.\\\\'), '.'); - -} else { - // On unix, backspace is a valid name component like any other character. - assert.equal(path.extname('.\\'), ''); - assert.equal(path.extname('..\\'), '.\\'); - assert.equal(path.extname('file.ext\\'), '.ext\\'); - assert.equal(path.extname('file.ext\\\\'), '.ext\\\\'); - assert.equal(path.extname('file\\'), ''); - assert.equal(path.extname('file\\\\'), ''); - assert.equal(path.extname('file.\\'), '.\\'); - assert.equal(path.extname('file.\\\\'), '.\\\\'); -} - -// path.join tests -var failures = []; -var joinTests = - // arguments result - [[['.', 'x/b', '..', '/b/c.js'], 'x/b/c.js'], - [['/.', 'x/b', '..', '/b/c.js'], '/x/b/c.js'], - [['/foo', '../../../bar'], '/bar'], - [['foo', '../../../bar'], '../../bar'], - [['foo/', '../../../bar'], '../../bar'], - [['foo/x', '../../../bar'], '../bar'], - [['foo/x', './bar'], 'foo/x/bar'], - [['foo/x/', './bar'], 'foo/x/bar'], - [['foo/x/', '.', 'bar'], 'foo/x/bar'], - [['./'], './'], - [['.', './'], './'], - [['.', '.', '.'], '.'], - [['.', './', '.'], '.'], - [['.', '/./', '.'], '.'], - [['.', '/////./', '.'], '.'], - [['.'], '.'], - [['', '.'], '.'], - [['', 'foo'], 'foo'], - [['foo', '/bar'], 'foo/bar'], - [['', '/foo'], '/foo'], - [['', '', '/foo'], '/foo'], - [['', '', 'foo'], 'foo'], - [['foo', ''], 'foo'], - [['foo/', ''], 'foo/'], - [['foo', '', '/bar'], 'foo/bar'], - [['./', '..', '/foo'], '../foo'], - [['./', '..', '..', '/foo'], '../../foo'], - [['.', '..', '..', '/foo'], '../../foo'], - [['', '..', '..', '/foo'], '../../foo'], - [['/'], '/'], - [['/', '.'], '/'], - [['/', '..'], '/'], - [['/', '..', '..'], '/'], - [[''], '.'], - [['', ''], '.'], - [[' /foo'], ' /foo'], - [[' ', 'foo'], ' /foo'], - [[' ', '.'], ' '], - [[' ', '/'], ' /'], - [[' ', ''], ' '], - [['/', 'foo'], '/foo'], - [['/', '/foo'], '/foo'], - [['/', '//foo'], '/foo'], - [['/', '', '/foo'], '/foo'], - [['', '/', 'foo'], '/foo'], - [['', '/', '/foo'], '/foo'] - ]; - -// Windows-specific join tests -if (isWindows) { - joinTests = joinTests.concat( - [// UNC path expected - [['//foo/bar'], '//foo/bar/'], - [['\\/foo/bar'], '//foo/bar/'], - [['\\\\foo/bar'], '//foo/bar/'], - // UNC path expected - server and share separate - [['//foo', 'bar'], '//foo/bar/'], - [['//foo/', 'bar'], '//foo/bar/'], - [['//foo', '/bar'], '//foo/bar/'], - // UNC path expected - questionable - [['//foo', '', 'bar'], '//foo/bar/'], - [['//foo/', '', 'bar'], '//foo/bar/'], - [['//foo/', '', '/bar'], '//foo/bar/'], - // UNC path expected - even more questionable - [['', '//foo', 'bar'], '//foo/bar/'], - [['', '//foo/', 'bar'], '//foo/bar/'], - [['', '//foo/', '/bar'], '//foo/bar/'], - // No UNC path expected (no double slash in first component) - [['\\', 'foo/bar'], '/foo/bar'], - [['\\', '/foo/bar'], '/foo/bar'], - [['', '/', '/foo/bar'], '/foo/bar'], - // No UNC path expected (no non-slashes in first component - questionable) - [['//', 'foo/bar'], '/foo/bar'], - [['//', '/foo/bar'], '/foo/bar'], - [['\\\\', '/', '/foo/bar'], '/foo/bar'], - [['//'], '/'], - // No UNC path expected (share name missing - questionable). - [['//foo'], '/foo'], - [['//foo/'], '/foo/'], - [['//foo', '/'], '/foo/'], - [['//foo', '', '/'], '/foo/'], - // No UNC path expected (too many leading slashes - questionable) - [['///foo/bar'], '/foo/bar'], - [['////foo', 'bar'], '/foo/bar'], - [['\\\\\\/foo/bar'], '/foo/bar'], - // Drive-relative vs drive-absolute paths. This merely describes the - // status quo, rather than being obviously right - [['c:'], 'c:.'], - [['c:.'], 'c:.'], - [['c:', ''], 'c:.'], - [['', 'c:'], 'c:.'], - [['c:.', '/'], 'c:./'], - [['c:.', 'file'], 'c:file'], - [['c:', '/'], 'c:/'], - [['c:', 'file'], 'c:/file'] - ]); -} - -// Run the join tests. -joinTests.forEach(function(test) { - var actual = path.join.apply(path, test[0]); - var expected = isWindows ? test[1].replace(/\//g, '\\') : test[1]; - var message = 'path.join(' + test[0].map(JSON.stringify).join(',') + ')' + - '\n expect=' + JSON.stringify(expected) + - '\n actual=' + JSON.stringify(actual); - if (actual !== expected) failures.push('\n' + message); - // assert.equal(actual, expected, message); -}); -assert.equal(failures.length, 0, failures.join('')); -var joinThrowTests = [true, false, 7, null, {}, undefined, [], NaN]; -joinThrowTests.forEach(function(test) { - assert.throws(function() { - path.join(test); - }, TypeError); - assert.throws(function() { - path.resolve(test); - }, TypeError); -}); - - -// path normalize tests -if (isWindows) { - assert.equal(path.normalize('./fixtures///b/../b/c.js'), - 'fixtures\\b\\c.js'); - assert.equal(path.normalize('/foo/../../../bar'), '\\bar'); - assert.equal(path.normalize('a//b//../b'), 'a\\b'); - assert.equal(path.normalize('a//b//./c'), 'a\\b\\c'); - assert.equal(path.normalize('a//b//.'), 'a\\b'); - assert.equal(path.normalize('//server/share/dir/file.ext'), - '\\\\server\\share\\dir\\file.ext'); -} else { - assert.equal(path.normalize('./fixtures///b/../b/c.js'), - 'fixtures/b/c.js'); - assert.equal(path.normalize('/foo/../../../bar'), '/bar'); - assert.equal(path.normalize('a//b//../b'), 'a/b'); - assert.equal(path.normalize('a//b//./c'), 'a/b/c'); - assert.equal(path.normalize('a//b//.'), 'a/b'); -} - -// path.resolve tests -if (isWindows) { - // windows - var resolveTests = - // arguments result - [[['c:/blah\\blah', 'd:/games', 'c:../a'], 'c:\\blah\\a'], - [['c:/ignore', 'd:\\a/b\\c/d', '\\e.exe'], 'd:\\e.exe'], - [['c:/ignore', 'c:/some/file'], 'c:\\some\\file'], - [['d:/ignore', 'd:some/dir//'], 'd:\\ignore\\some\\dir'], - [['.'], process.cwd()], - [['//server/share', '..', 'relative\\'], '\\\\server\\share\\relative'], - [['c:/', '//'], 'c:\\'], - [['c:/', '//dir'], 'c:\\dir'], - [['c:/', '//server/share'], '\\\\server\\share\\'], - [['c:/', '//server//share'], '\\\\server\\share\\'], - [['c:/', '///some//dir'], 'c:\\some\\dir'] - ]; -} else { - // Posix - var resolveTests = - // arguments result - [[['/var/lib', '../', 'file/'], '/var/file'], - [['/var/lib', '/../', 'file/'], '/file'], - // For some mysterious reasons OSX debug builds resolve incorrectly - // https://tbpl.mozilla.org/php/getParsedLog.php?id=25105489&tree=Mozilla-Inbound - // Disable this tests until Bug 891698 is fixed. - // [['a/b/c/', '../../..'], process.cwd()], - // [['.'], process.cwd()], - [['/some/dir', '.', '/absolute/'], '/absolute']]; -} -var failures = []; -resolveTests.forEach(function(test) { - var actual = path.resolve.apply(path, test[0]); - var expected = test[1]; - var message = 'path.resolve(' + test[0].map(JSON.stringify).join(',') + ')' + - '\n expect=' + JSON.stringify(expected) + - '\n actual=' + JSON.stringify(actual); - if (actual !== expected) failures.push('\n' + message); - // assert.equal(actual, expected, message); -}); -assert.equal(failures.length, 0, failures.join('')); - -// path.isAbsolute tests -if (isWindows) { - assert.equal(path.isAbsolute('//server/file'), true); - assert.equal(path.isAbsolute('\\\\server\\file'), true); - assert.equal(path.isAbsolute('C:/Users/'), true); - assert.equal(path.isAbsolute('C:\\Users\\'), true); - assert.equal(path.isAbsolute('C:cwd/another'), false); - assert.equal(path.isAbsolute('C:cwd\\another'), false); - assert.equal(path.isAbsolute('directory/directory'), false); - assert.equal(path.isAbsolute('directory\\directory'), false); -} else { - assert.equal(path.isAbsolute('/home/foo'), true); - assert.equal(path.isAbsolute('/home/foo/..'), true); - assert.equal(path.isAbsolute('bar/'), false); - assert.equal(path.isAbsolute('./baz'), false); -} - -// path.relative tests -if (isWindows) { - // windows - var relativeTests = - // arguments result - [['c:/blah\\blah', 'd:/games', 'd:\\games'], - ['c:/aaaa/bbbb', 'c:/aaaa', '..'], - ['c:/aaaa/bbbb', 'c:/cccc', '..\\..\\cccc'], - ['c:/aaaa/bbbb', 'c:/aaaa/bbbb', ''], - ['c:/aaaa/bbbb', 'c:/aaaa/cccc', '..\\cccc'], - ['c:/aaaa/', 'c:/aaaa/cccc', 'cccc'], - ['c:/', 'c:\\aaaa\\bbbb', 'aaaa\\bbbb'], - ['c:/aaaa/bbbb', 'd:\\', 'd:\\']]; -} else { - // posix - var relativeTests = - // arguments result - [['/var/lib', '/var', '..'], - ['/var/lib', '/bin', '../../bin'], - ['/var/lib', '/var/lib', ''], - ['/var/lib', '/var/apache', '../apache'], - ['/var/', '/var/lib', 'lib'], - ['/', '/var/lib', 'var/lib']]; -} -var failures = []; -relativeTests.forEach(function(test) { - var actual = path.relative(test[0], test[1]); - var expected = test[2]; - var message = 'path.relative(' + - test.slice(0, 2).map(JSON.stringify).join(',') + - ')' + - '\n expect=' + JSON.stringify(expected) + - '\n actual=' + JSON.stringify(actual); - if (actual !== expected) failures.push('\n' + message); -}); -assert.equal(failures.length, 0, failures.join('')); - -// path.sep tests -if (isWindows) { - // windows - assert.equal(path.sep, '\\'); -} else { - // posix - assert.equal(path.sep, '/'); -} - -// path.delimiter tests -if (isWindows) { - // windows - assert.equal(path.delimiter, ';'); -} else { - // posix - assert.equal(path.delimiter, ':'); -} - -}; - -require('test').run(exports); +module.exports = require("./path/test-path.js"); diff --git a/addon-sdk/source/test/test-preferences-service.js b/addon-sdk/source/test/test-preferences-service.js index b95eb51b9ff9..6788c23b639d 100644 --- a/addon-sdk/source/test/test-preferences-service.js +++ b/addon-sdk/source/test/test-preferences-service.js @@ -3,9 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; +const { Cc, Ci, Cu } = require("chrome"); const prefs = require("sdk/preferences/service"); const Branch = prefs.Branch; -const { Cc, Ci, Cu } = require("chrome"); const BundleService = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService); const specialChars = "!@#$%^&*()_-=+[]{}~`\'\"<>,./?;:"; @@ -35,41 +35,42 @@ exports.testGetAndSet = function(assert) { assert.equal( prefs.keys("test_set_get_pref").sort().toString(), - ["test_set_get_pref.integer","test_set_get_pref"].sort().toString()); + ["test_set_get_pref.integer","test_set_get_pref"].sort().toString(), + "the key list is correct"); prefs.set("test_set_get_number_pref", 42); assert.throws( - function() { prefs.set("test_set_get_number_pref", 3.14159); }, + () => prefs.set("test_set_get_number_pref", 3.14159), /cannot store non-integer number: 3.14159/, "setting a float preference should raise an error" ); - assert.equal(prefs.get("test_set_get_number_pref"), 42, - "bad-type write attempt should not overwrite"); + assert.equal(prefs.get("test_set_get_number_pref"), + 42, + "bad-type write attempt should not overwrite"); - // 0x80000000 (no), 0x7fffffff (yes), -0x80000000 (yes), -0x80000001 (no) + // 0x80000000 (bad), 0x7fffffff (ok), -0x80000000 (ok), -0x80000001 (bad) assert.throws( - function() { prefs.set("test_set_get_number_pref", Math.pow(2, 31)); }, - new RegExp("you cannot set the test_set_get_number_pref pref to the number " + - "2147483648, as number pref values must be in the signed 32\\-bit " + - "integer range \\-\\(2\\^31\\) to 2\\^31\\-1. To store numbers outside that " + - "range, store them as strings."), - "setting an int pref outside the range -(2^31) to 2^31-1 shouldn't work" + () => prefs.set("test_set_get_number_pref", 0x80000000), + /32\-bit/, + "setting an int pref above 2^31-1 shouldn't work" ); + assert.equal(prefs.get("test_set_get_number_pref"), 42, "out-of-range write attempt should not overwrite 1"); - prefs.set("test_set_get_number_pref", Math.pow(2, 31)-1); - assert.equal(prefs.get("test_set_get_number_pref"), 0x7fffffff, - "in-range write attempt should work 1"); - prefs.set("test_set_get_number_pref", -Math.pow(2, 31)); - assert.equal(prefs.get("test_set_get_number_pref"), -0x80000000, - "in-range write attempt should work 2"); + + prefs.set("test_set_get_number_pref", 0x7fffffff); + assert.equal(prefs.get("test_set_get_number_pref"), + 0x7fffffff, + "in-range write attempt should work 1"); + + prefs.set("test_set_get_number_pref", -0x80000000); + assert.equal(prefs.get("test_set_get_number_pref"), + -0x80000000, + "in-range write attempt should work 2"); assert.throws( - function() { prefs.set("test_set_get_number_pref", -0x80000001); }, - new RegExp("you cannot set the test_set_get_number_pref pref to the number " + - "\\-2147483649, as number pref values must be in the signed 32-bit " + - "integer range \\-\\(2\\^31\\) to 2\\^31\\-1. To store numbers outside that " + - "range, store them as strings."), - "setting an int pref outside the range -(2^31) to 2^31-1 shouldn't work" + () => prefs.set("test_set_get_number_pref", -0x80000001), + /32\-bit/, + "setting an int pref below -(2^31) shouldn't work" ); assert.equal(prefs.get("test_set_get_number_pref"), -0x80000000, "out-of-range write attempt should not overwrite 2"); @@ -88,16 +89,14 @@ exports.testGetAndSet = function(assert) { String.fromCharCode(960), "set/get unicode preference should work"); - var unsupportedValues = [null, [], undefined]; - unsupportedValues.forEach( - function(value) { - assert.throws( - function() { prefs.set("test_set_pref", value); }, - new RegExp("can't set pref test_set_pref to value '" + value + "'; " + - "it isn't a string, integer, or boolean"), - "Setting a pref to " + uneval(value) + " should raise error" - ); - }); + [ null, [], undefined ].forEach((value) => { + assert.throws( + () => prefs.set("test_set_pref", value), + new RegExp("can't set pref test_set_pref to value '" + value + "'; " + + "it isn't a string, number, or boolean", "i"), + "Setting a pref to " + uneval(value) + " should raise error" + ); + }); }; exports.testPrefClass = function(assert) { @@ -135,11 +134,11 @@ exports.testSpecialChars = function(assert) { let chars = specialChars.split(''); const ROOT = "test."; - chars.forEach(function(char) { + chars.forEach((char) => { let rand = Math.random() + ""; - prefs.set(ROOT+char, rand); + prefs.set(ROOT + char, rand); assert.equal(prefs.get(ROOT+char), rand, "setting pref with a name that is a special char, " + char + ", worked!"); }); }; -require('sdk/test').run(exports); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-private-browsing.js b/addon-sdk/source/test/test-private-browsing.js index 0e5754ef87b4..7c0db6a7d526 100644 --- a/addon-sdk/source/test/test-private-browsing.js +++ b/addon-sdk/source/test/test-private-browsing.js @@ -89,4 +89,4 @@ exports.testNewGlobalPBService = function(assert) { assert.equal(isPrivate(), false, 'isPrivate() is false again'); }; -require('sdk/test').run(exports); +require('sdk/test').run(module.exports); diff --git a/addon-sdk/source/test/test-promise.js b/addon-sdk/source/test/test-promise.js index 812ed6e4de7b..3174c8793f78 100644 --- a/addon-sdk/source/test/test-promise.js +++ b/addon-sdk/source/test/test-promise.js @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - 'use strict'; const { Cc, Cu, Ci } = require('chrome'); diff --git a/addon-sdk/source/test/test-querystring.js b/addon-sdk/source/test/test-querystring.js index abedb087afb6..b108cd0818f7 100644 --- a/addon-sdk/source/test/test-querystring.js +++ b/addon-sdk/source/test/test-querystring.js @@ -1,205 +1,6 @@ -// Copyright Joyent, Inc. and other Node contributors. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the -// following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -// USE OR OTHER DEALINGS IN THE SOFTWARE. - +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -// test using assert -var qs = require('sdk/querystring'); - -// folding block, commented to pass gjslint -// {{{ -// [ wonkyQS, canonicalQS, obj ] -var qsTestCases = [ - ['foo=918854443121279438895193', - 'foo=918854443121279438895193', - {'foo': '918854443121279438895193'}], - ['foo=bar', 'foo=bar', {'foo': 'bar'}], - //['foo=bar&foo=quux', 'foo=bar&foo=quux', {'foo': ['bar', 'quux']}], - ['foo=1&bar=2', 'foo=1&bar=2', {'foo': '1', 'bar': '2'}], - // ['my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F', - // 'my%20weird%20field=q1!2%22\'w%245%267%2Fz8)%3F', - // {'my weird field': 'q1!2"\'w$5&7/z8)?' }], - ['foo%3Dbaz=bar', 'foo%3Dbaz=bar', {'foo=baz': 'bar'}], - ['foo=baz=bar', 'foo=baz%3Dbar', {'foo': 'baz=bar'}], - /* - ['str=foo&arr=1&arr=2&arr=3&somenull=&undef=', - 'str=foo&arr=1&arr=2&arr=3&somenull=&undef=', - { 'str': 'foo', - 'arr': ['1', '2', '3'], - 'somenull': '', - 'undef': ''}], - */ - //[' foo = bar ', '%20foo%20=%20bar%20', {' foo ': ' bar '}], - // disable test that fails ['foo=%zx', 'foo=%25zx', {'foo': '%zx'}], - ['foo=%EF%BF%BD', 'foo=%EF%BF%BD', {'foo': '\ufffd' }] -]; - -// [ wonkyQS, canonicalQS, obj ] -var qsColonTestCases = [ - ['foo:bar', 'foo:bar', {'foo': 'bar'}], - //['foo:bar;foo:quux', 'foo:bar;foo:quux', {'foo': ['bar', 'quux']}], - ['foo:1&bar:2;baz:quux', - 'foo:1%26bar%3A2;baz:quux', - {'foo': '1&bar:2', 'baz': 'quux'}], - ['foo%3Abaz:bar', 'foo%3Abaz:bar', {'foo:baz': 'bar'}], - ['foo:baz:bar', 'foo:baz%3Abar', {'foo': 'baz:bar'}] -]; - -// [wonkyObj, qs, canonicalObj] -var extendedFunction = function() {}; -extendedFunction.prototype = {a: 'b'}; -var qsWeirdObjects = [ - //[{regexp: /./g}, 'regexp=', {'regexp': ''}], - //[{regexp: new RegExp('.', 'g')}, 'regexp=', {'regexp': ''}], - //[{fn: function() {}}, 'fn=', {'fn': ''}], - //[{fn: new Function('')}, 'fn=', {'fn': ''}], - //[{math: Math}, 'math=', {'math': ''}], - //[{e: extendedFunction}, 'e=', {'e': ''}], - //[{d: new Date()}, 'd=', {'d': ''}], - //[{d: Date}, 'd=', {'d': ''}], - //[{f: new Boolean(false), t: new Boolean(true)}, 'f=&t=', {'f': '', 't': ''}], - [{f: false, t: true}, 'f=false&t=true', {'f': 'false', 't': 'true'}], - //[{n: null}, 'n=', {'n': ''}], - //[{nan: NaN}, 'nan=', {'nan': ''}], - //[{inf: Infinity}, 'inf=', {'inf': ''}] -]; -// }}} - -var qsNoMungeTestCases = [ - ['', {}], - //['foo=bar&foo=baz', {'foo': ['bar', 'baz']}], - ['blah=burp', {'blah': 'burp'}], - //['gragh=1&gragh=3&goo=2', {'gragh': ['1', '3'], 'goo': '2'}], - ['frappucino=muffin&goat%5B%5D=scone&pond=moose', - {'frappucino': 'muffin', 'goat[]': 'scone', 'pond': 'moose'}], - ['trololol=yes&lololo=no', {'trololol': 'yes', 'lololo': 'no'}] -]; - -exports['test basic'] = function(assert) { - assert.strictEqual('918854443121279438895193', - qs.parse('id=918854443121279438895193').id, - 'prase id=918854443121279438895193'); -}; - -exports['test that the canonical qs is parsed properly'] = function(assert) { - qsTestCases.forEach(function(testCase) { - assert.deepEqual(testCase[2], qs.parse(testCase[0]), - 'parse ' + testCase[0]); - }); -}; - - -exports['test that the colon test cases can do the same'] = function(assert) { - qsColonTestCases.forEach(function(testCase) { - assert.deepEqual(testCase[2], qs.parse(testCase[0], ';', ':'), - 'parse ' + testCase[0] + ' -> ; :'); - }); -}; - -exports['test the weird objects, that they get parsed properly'] = function(assert) { - qsWeirdObjects.forEach(function(testCase) { - assert.deepEqual(testCase[2], qs.parse(testCase[1]), - 'parse ' + testCase[1]); - }); -}; - -exports['test non munge test cases'] = function(assert) { - qsNoMungeTestCases.forEach(function(testCase) { - assert.deepEqual(testCase[0], qs.stringify(testCase[1], '&', '=', false), - 'stringify ' + JSON.stringify(testCase[1]) + ' -> & ='); - }); -}; - -exports['test the nested qs-in-qs case'] = function(assert) { - var f = qs.parse('a=b&q=x%3Dy%26y%3Dz'); - f.q = qs.parse(f.q); - assert.deepEqual(f, { a: 'b', q: { x: 'y', y: 'z' } }, - 'parse a=b&q=x%3Dy%26y%3Dz'); -}; - -exports['test nested in colon'] = function(assert) { - var f = qs.parse('a:b;q:x%3Ay%3By%3Az', ';', ':'); - f.q = qs.parse(f.q, ';', ':'); - assert.deepEqual(f, { a: 'b', q: { x: 'y', y: 'z' } }, - 'parse a:b;q:x%3Ay%3By%3Az -> ; :'); -}; - -exports['test stringifying'] = function(assert) { - qsTestCases.forEach(function(testCase) { - assert.equal(testCase[1], qs.stringify(testCase[2]), - 'stringify ' + JSON.stringify(testCase[2])); - }); - - qsColonTestCases.forEach(function(testCase) { - assert.equal(testCase[1], qs.stringify(testCase[2], ';', ':'), - 'stringify ' + JSON.stringify(testCase[2]) + ' -> ; :'); - }); - - qsWeirdObjects.forEach(function(testCase) { - assert.equal(testCase[1], qs.stringify(testCase[0]), - 'stringify ' + JSON.stringify(testCase[0])); - }); -}; - -exports['test stringifying nested'] = function(assert) { - var f = qs.stringify({ - a: 'b', - q: qs.stringify({ - x: 'y', - y: 'z' - }) - }); - assert.equal(f, 'a=b&q=x%3Dy%26y%3Dz', - JSON.stringify({ - a: 'b', - 'qs.stringify -> q': { - x: 'y', - y: 'z' - } - })); - - var threw = false; - try { qs.parse(undefined); } catch(error) { threw = true; } - assert.ok(!threw, "does not throws on undefined"); -}; - -exports['test nested in colon'] = function(assert) { - var f = qs.stringify({ - a: 'b', - q: qs.stringify({ - x: 'y', - y: 'z' - }, ';', ':') - }, ';', ':'); - assert.equal(f, 'a:b;q:x%3Ay%3By%3Az', - 'stringify ' + JSON.stringify({ - a: 'b', - 'qs.stringify -> q': { - x: 'y', - y: 'z' - } - }) + ' -> ; : '); - - - assert.deepEqual({}, qs.parse(), 'parse undefined'); -}; - -require("test").run(exports); +module.exports = require("./querystring/test-querystring.js"); diff --git a/addon-sdk/source/test/test-registry.js b/addon-sdk/source/test/test-registry.js deleted file mode 100644 index 3626efc70c9c..000000000000 --- a/addon-sdk/source/test/test-registry.js +++ /dev/null @@ -1,82 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -'use strict'; - -exports['test:add'] = function(assert) { - function Class() {} - let fixture = require('sdk/util/registry').Registry(Class); - let isAddEmitted = false; - fixture.on('add', function(item) { - assert.ok( - item instanceof Class, - 'if object added is not an instance should construct instance from it' - ); - assert.ok( - fixture.has(item), - 'callback is called after instance is added' - ); - assert.ok( - !isAddEmitted, - 'callback should be called for the same item only once' - ); - isAddEmitted = true; - }); - - let object = fixture.add({}); - fixture.add(object); -}; - -exports['test:remove'] = function(assert) { - function Class() {} - let fixture = require('sdk/util/registry').Registry(Class); - fixture.on('remove', function(item) { - assert.ok( - item instanceof Class, - 'if object removed can be only instance of Class' - ); - assert.ok( - fixture.has(item), - 'callback is called before instance is removed' - ); - assert.ok( - !isRemoveEmitted, - 'callback should be called for the same item only once' - ); - isRemoveEmitted = true; - }); - - fixture.remove({}); - let object = fixture.add({}); - fixture.remove(object); - fixture.remove(object); -}; - -exports['test:items'] = function(assert) { - function Class() {} - let fixture = require('sdk/util/registry').Registry(Class), - actual, - times = 0; - - function testItem(item) { - times ++; - assert.equal( - actual, - item, - 'item should match actual item being added/removed' - ); - } - - actual = fixture.add({}); - - fixture.on('add', testItem); - fixture.on('remove', testItem); - - fixture.remove(actual); - fixture.remove(fixture.add(actual = new Class())); - assert.equal(3, times, 'should notify listeners on each call'); -}; - -require('sdk/test').run(exports); - diff --git a/addon-sdk/source/test/test-require.js b/addon-sdk/source/test/test-require.js index dc73ebc674a3..b1f6e509230c 100644 --- a/addon-sdk/source/test/test-require.js +++ b/addon-sdk/source/test/test-require.js @@ -1,11 +1,10 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - 'use strict'; const traceback = require('sdk/console/traceback'); -const REQUIRE_LINE_NO = 30; +const REQUIRE_LINE_NO = 29; exports.test_no_args = function(assert) { let passed = tryRequireModule(assert); @@ -28,7 +27,8 @@ function tryRequireModule(assert, module) { try { // This line number is important, referenced in REQUIRE_LINE_NO let doesNotExist = require(module); - } catch(e) { + } + catch(e) { checkError(assert, module, e); passed = true; } @@ -64,4 +64,4 @@ function checkError (assert, name, e) { 'stacktrace has correct line number'); } -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-rules.js b/addon-sdk/source/test/test-rules.js index 6b5c2db06341..07f780645958 100644 --- a/addon-sdk/source/test/test-rules.js +++ b/addon-sdk/source/test/test-rules.js @@ -76,4 +76,4 @@ exports.testIterable = function(test) { test.equal(rules[i], ['*.mozilla.org', 'data:*', 'http://addons.mozilla.org'][i]); }; -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-sandbox.js b/addon-sdk/source/test/test-sandbox.js index d7f2e5ac6e50..ad342b8c5ca7 100644 --- a/addon-sdk/source/test/test-sandbox.js +++ b/addon-sdk/source/test/test-sandbox.js @@ -163,4 +163,4 @@ exports['test nuke sandbox'] = function(assert) { ); } -require('test').run(exports); +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-selection.js b/addon-sdk/source/test/test-selection.js index 35856ee35921..a0d5ff894104 100644 --- a/addon-sdk/source/test/test-selection.js +++ b/addon-sdk/source/test/test-selection.js @@ -22,17 +22,19 @@ const URL = "data:text/html;charset=utf-8," + encodeURIComponent(HTML); const FRAME_HTML = "