From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- addon-sdk/source/.gitattributes | 5 + addon-sdk/source/.gitignore | 36 + addon-sdk/source/.jpmignore | 18 + addon-sdk/source/.travis.yml | 26 + addon-sdk/source/CONTRIBUTING.md | 54 + addon-sdk/source/LICENSE | 30 + addon-sdk/source/README.md | 34 + addon-sdk/source/app-extension/application.ini | 11 + addon-sdk/source/app-extension/bootstrap.js | 362 ++ addon-sdk/source/app-extension/install.rdf | 33 + addon-sdk/source/bin/activate | 84 + addon-sdk/source/bin/activate.bat | 134 + addon-sdk/source/bin/activate.fish | 66 + addon-sdk/source/bin/activate.ps1 | 99 + addon-sdk/source/bin/cfx | 33 + addon-sdk/source/bin/cfx.bat | 6 + addon-sdk/source/bin/deactivate.bat | 23 + addon-sdk/source/bin/fx-download.sh | 7 + .../integration-scripts/buildbot-run-cfx-helper | 14 + .../bin/integration-scripts/integration-check | 364 ++ addon-sdk/source/bin/jpm-test.js | 34 + addon-sdk/source/bin/node-scripts/apply-patch.js | 64 + addon-sdk/source/bin/node-scripts/test.addons.js | 57 + addon-sdk/source/bin/node-scripts/test.docs.js | 145 + addon-sdk/source/bin/node-scripts/test.examples.js | 45 + .../source/bin/node-scripts/test.firefox-bin.js | 37 + addon-sdk/source/bin/node-scripts/test.ini.js | 68 + addon-sdk/source/bin/node-scripts/test.modules.js | 28 + addon-sdk/source/bin/node-scripts/update-ini.js | 141 + addon-sdk/source/bin/node-scripts/utils.js | 104 + addon-sdk/source/bin/node-scripts/words.txt | 11 + addon-sdk/source/examples/actor-repl/README.md | 3 + .../actor-repl/data/codemirror-compressed.js | 5 + .../source/examples/actor-repl/data/codemirror.css | 264 + .../source/examples/actor-repl/data/index.html | 147 + addon-sdk/source/examples/actor-repl/data/main.css | 117 + .../source/examples/actor-repl/data/robot.png | Bin 0 -> 4184 bytes addon-sdk/source/examples/actor-repl/index.js | 37 + addon-sdk/source/examples/actor-repl/package.json | 10 + .../source/examples/actor-repl/test/test-main.js | 10 + .../source/examples/debug-client/data/client.js | 816 +++ .../source/examples/debug-client/data/index.html | 50 + .../source/examples/debug-client/data/plugin.png | Bin 0 -> 3819 bytes .../source/examples/debug-client/data/task.js | 28 + addon-sdk/source/examples/debug-client/index.js | 33 + .../source/examples/debug-client/package.json | 10 + .../source/examples/debug-client/test/test-main.js | 10 + .../source/examples/reading-data/data/mom.png | Bin 0 -> 4778 bytes .../source/examples/reading-data/data/sample.html | 7 + addon-sdk/source/examples/reading-data/lib/main.js | 53 + .../source/examples/reading-data/package.json | 9 + .../examples/reading-data/tests/test-main.js | 25 + addon-sdk/source/examples/theme/data/icon-16.png | Bin 0 -> 1657 bytes addon-sdk/source/examples/theme/data/index.html | 9 + addon-sdk/source/examples/theme/data/theme.css | 7 + addon-sdk/source/examples/theme/lib/main.js | 37 + addon-sdk/source/examples/theme/package.json | 10 + addon-sdk/source/examples/theme/test/test-main.js | 10 + .../source/examples/toolbar-api/data/favicon.ico | Bin 0 -> 15086 bytes .../source/examples/toolbar-api/data/index.html | 21 + addon-sdk/source/examples/toolbar-api/lib/main.js | 48 + addon-sdk/source/examples/toolbar-api/package.json | 12 + .../source/examples/toolbar-api/test/test-main.js | 10 + .../source/examples/ui-button-apis/lib/main.js | 39 + .../source/examples/ui-button-apis/package.json | 10 + .../examples/ui-button-apis/tests/test-main.js | 29 + addon-sdk/source/gulpfile.js | 44 + addon-sdk/source/lib/dev/debuggee.js | 95 + addon-sdk/source/lib/dev/frame-script.js | 120 + addon-sdk/source/lib/dev/panel.js | 259 + addon-sdk/source/lib/dev/panel/view.js | 14 + addon-sdk/source/lib/dev/ports.js | 64 + addon-sdk/source/lib/dev/theme.js | 135 + addon-sdk/source/lib/dev/theme/hooks.js | 17 + addon-sdk/source/lib/dev/toolbox.js | 107 + addon-sdk/source/lib/dev/utils.js | 40 + addon-sdk/source/lib/dev/volcan.js | 3848 +++++++++++++++ addon-sdk/source/lib/diffpatcher/.travis.yml | 5 + addon-sdk/source/lib/diffpatcher/History.md | 14 + addon-sdk/source/lib/diffpatcher/License.md | 18 + addon-sdk/source/lib/diffpatcher/Readme.md | 70 + addon-sdk/source/lib/diffpatcher/diff.js | 45 + addon-sdk/source/lib/diffpatcher/index.js | 5 + addon-sdk/source/lib/diffpatcher/package.json | 54 + addon-sdk/source/lib/diffpatcher/patch.js | 21 + addon-sdk/source/lib/diffpatcher/rebase.js | 36 + addon-sdk/source/lib/diffpatcher/test/common.js | 3 + addon-sdk/source/lib/diffpatcher/test/diff.js | 59 + addon-sdk/source/lib/diffpatcher/test/index.js | 14 + addon-sdk/source/lib/diffpatcher/test/patch.js | 83 + addon-sdk/source/lib/diffpatcher/test/tap.js | 3 + .../source/lib/framescript/FrameScriptManager.jsm | 27 + addon-sdk/source/lib/framescript/content.jsm | 94 + addon-sdk/source/lib/framescript/context-menu.js | 215 + addon-sdk/source/lib/framescript/manager.js | 26 + addon-sdk/source/lib/framescript/util.js | 25 + addon-sdk/source/lib/index.js | 3 + addon-sdk/source/lib/jetpack-id/index.js | 53 + addon-sdk/source/lib/jetpack-id/package.json | 28 + addon-sdk/source/lib/method/.travis.yml | 5 + addon-sdk/source/lib/method/History.md | 55 + addon-sdk/source/lib/method/License.md | 18 + addon-sdk/source/lib/method/Readme.md | 117 + addon-sdk/source/lib/method/core.js | 225 + addon-sdk/source/lib/method/package.json | 41 + addon-sdk/source/lib/method/test/browser.js | 20 + addon-sdk/source/lib/method/test/common.js | 272 + .../source/lib/mozilla-toolkit-versioning/index.js | 112 + .../lib/mozilla-toolkit-versioning/lib/utils.js | 15 + .../lib/mozilla-toolkit-versioning/package.json | 21 + addon-sdk/source/lib/node/os.js | 90 + addon-sdk/source/lib/sdk/addon/bootstrap.js | 182 + addon-sdk/source/lib/sdk/addon/events.js | 56 + addon-sdk/source/lib/sdk/addon/host.js | 12 + addon-sdk/source/lib/sdk/addon/installer.js | 121 + addon-sdk/source/lib/sdk/addon/manager.js | 18 + addon-sdk/source/lib/sdk/addon/runner.js | 180 + addon-sdk/source/lib/sdk/addon/window.js | 66 + addon-sdk/source/lib/sdk/base64.js | 47 + addon-sdk/source/lib/sdk/browser/events.js | 20 + addon-sdk/source/lib/sdk/clipboard.js | 337 ++ addon-sdk/source/lib/sdk/console/plain-text.js | 78 + addon-sdk/source/lib/sdk/console/traceback.js | 86 + addon-sdk/source/lib/sdk/content/content-worker.js | 305 ++ addon-sdk/source/lib/sdk/content/content.js | 17 + addon-sdk/source/lib/sdk/content/context-menu.js | 408 ++ addon-sdk/source/lib/sdk/content/events.js | 57 + addon-sdk/source/lib/sdk/content/l10n-html.js | 133 + addon-sdk/source/lib/sdk/content/loader.js | 74 + addon-sdk/source/lib/sdk/content/mod.js | 68 + addon-sdk/source/lib/sdk/content/page-mod.js | 236 + addon-sdk/source/lib/sdk/content/page-worker.js | 154 + addon-sdk/source/lib/sdk/content/sandbox.js | 426 ++ addon-sdk/source/lib/sdk/content/sandbox/events.js | 12 + addon-sdk/source/lib/sdk/content/tab-events.js | 58 + addon-sdk/source/lib/sdk/content/thumbnail.js | 51 + addon-sdk/source/lib/sdk/content/utils.js | 105 + addon-sdk/source/lib/sdk/content/worker-child.js | 158 + addon-sdk/source/lib/sdk/content/worker.js | 180 + addon-sdk/source/lib/sdk/context-menu.js | 1188 +++++ addon-sdk/source/lib/sdk/context-menu/context.js | 147 + addon-sdk/source/lib/sdk/context-menu/core.js | 384 ++ addon-sdk/source/lib/sdk/context-menu/readers.js | 112 + addon-sdk/source/lib/sdk/context-menu@2.js | 32 + addon-sdk/source/lib/sdk/core/disposable.js | 186 + addon-sdk/source/lib/sdk/core/heritage.js | 184 + addon-sdk/source/lib/sdk/core/namespace.js | 43 + addon-sdk/source/lib/sdk/core/observer.js | 89 + addon-sdk/source/lib/sdk/core/promise.js | 118 + addon-sdk/source/lib/sdk/core/reference.js | 29 + addon-sdk/source/lib/sdk/deprecated/api-utils.js | 197 + .../source/lib/sdk/deprecated/events/assembler.js | 54 + addon-sdk/source/lib/sdk/deprecated/sync-worker.js | 288 ++ .../source/lib/sdk/deprecated/unit-test-finder.js | 199 + addon-sdk/source/lib/sdk/deprecated/unit-test.js | 584 +++ .../source/lib/sdk/deprecated/window-utils.js | 193 + addon-sdk/source/lib/sdk/dom/events-shimmed.js | 18 + addon-sdk/source/lib/sdk/dom/events.js | 192 + addon-sdk/source/lib/sdk/dom/events/keys.js | 63 + addon-sdk/source/lib/sdk/event/chrome.js | 65 + addon-sdk/source/lib/sdk/event/core.js | 193 + addon-sdk/source/lib/sdk/event/dom.js | 78 + addon-sdk/source/lib/sdk/event/target.js | 74 + addon-sdk/source/lib/sdk/event/utils.js | 328 ++ addon-sdk/source/lib/sdk/frame/hidden-frame.js | 115 + addon-sdk/source/lib/sdk/frame/utils.js | 94 + addon-sdk/source/lib/sdk/fs/path.js | 500 ++ addon-sdk/source/lib/sdk/hotkeys.js | 40 + addon-sdk/source/lib/sdk/indexed-db.js | 79 + addon-sdk/source/lib/sdk/input/browser.js | 73 + addon-sdk/source/lib/sdk/input/customizable-ui.js | 28 + addon-sdk/source/lib/sdk/input/frame.js | 85 + addon-sdk/source/lib/sdk/input/system.js | 113 + addon-sdk/source/lib/sdk/io/buffer.js | 351 ++ addon-sdk/source/lib/sdk/io/byte-streams.js | 104 + addon-sdk/source/lib/sdk/io/file.js | 196 + addon-sdk/source/lib/sdk/io/fs.js | 984 ++++ addon-sdk/source/lib/sdk/io/stream.js | 440 ++ addon-sdk/source/lib/sdk/io/text-streams.js | 235 + addon-sdk/source/lib/sdk/keyboard/hotkeys.js | 110 + addon-sdk/source/lib/sdk/keyboard/observer.js | 58 + addon-sdk/source/lib/sdk/keyboard/utils.js | 189 + addon-sdk/source/lib/sdk/l10n.js | 91 + addon-sdk/source/lib/sdk/l10n/core.js | 9 + addon-sdk/source/lib/sdk/l10n/html.js | 32 + addon-sdk/source/lib/sdk/l10n/json/core.js | 36 + addon-sdk/source/lib/sdk/l10n/loader.js | 70 + addon-sdk/source/lib/sdk/l10n/locale.js | 127 + addon-sdk/source/lib/sdk/l10n/plural-rules.js | 407 ++ addon-sdk/source/lib/sdk/l10n/prefs.js | 51 + addon-sdk/source/lib/sdk/l10n/properties/core.js | 87 + addon-sdk/source/lib/sdk/lang/functional.js | 47 + .../source/lib/sdk/lang/functional/concurrent.js | 110 + addon-sdk/source/lib/sdk/lang/functional/core.js | 290 ++ .../source/lib/sdk/lang/functional/helpers.js | 29 + addon-sdk/source/lib/sdk/lang/type.js | 388 ++ addon-sdk/source/lib/sdk/lang/weak-set.js | 75 + addon-sdk/source/lib/sdk/loader/cuddlefish.js | 102 + addon-sdk/source/lib/sdk/loader/sandbox.js | 74 + addon-sdk/source/lib/sdk/messaging.js | 12 + addon-sdk/source/lib/sdk/model/core.js | 23 + addon-sdk/source/lib/sdk/net/url.js | 94 + addon-sdk/source/lib/sdk/net/xhr.js | 36 + addon-sdk/source/lib/sdk/notifications.js | 112 + addon-sdk/source/lib/sdk/output/system.js | 71 + addon-sdk/source/lib/sdk/page-mod.js | 190 + addon-sdk/source/lib/sdk/page-mod/match-pattern.js | 10 + addon-sdk/source/lib/sdk/page-worker.js | 194 + addon-sdk/source/lib/sdk/panel.js | 427 ++ addon-sdk/source/lib/sdk/panel/events.js | 27 + addon-sdk/source/lib/sdk/panel/utils.js | 451 ++ addon-sdk/source/lib/sdk/passwords.js | 61 + addon-sdk/source/lib/sdk/passwords/utils.js | 107 + addon-sdk/source/lib/sdk/places/bookmarks.js | 395 ++ addon-sdk/source/lib/sdk/places/contract.js | 73 + addon-sdk/source/lib/sdk/places/events.js | 128 + addon-sdk/source/lib/sdk/places/favicon.js | 49 + addon-sdk/source/lib/sdk/places/history.js | 65 + .../source/lib/sdk/places/host/host-bookmarks.js | 238 + addon-sdk/source/lib/sdk/places/host/host-query.js | 179 + addon-sdk/source/lib/sdk/places/host/host-tags.js | 92 + addon-sdk/source/lib/sdk/places/utils.js | 268 + addon-sdk/source/lib/sdk/platform/xpcom.js | 241 + .../source/lib/sdk/preferences/event-target.js | 61 + .../source/lib/sdk/preferences/native-options.js | 193 + addon-sdk/source/lib/sdk/preferences/service.js | 137 + addon-sdk/source/lib/sdk/preferences/utils.js | 42 + addon-sdk/source/lib/sdk/private-browsing.js | 12 + addon-sdk/source/lib/sdk/private-browsing/utils.js | 54 + addon-sdk/source/lib/sdk/querystring.js | 121 + addon-sdk/source/lib/sdk/remote/child.js | 284 ++ addon-sdk/source/lib/sdk/remote/core.js | 8 + addon-sdk/source/lib/sdk/remote/parent.js | 338 ++ addon-sdk/source/lib/sdk/remote/utils.js | 39 + addon-sdk/source/lib/sdk/request.js | 248 + addon-sdk/source/lib/sdk/selection.js | 470 ++ addon-sdk/source/lib/sdk/self.js | 61 + addon-sdk/source/lib/sdk/simple-prefs.js | 26 + addon-sdk/source/lib/sdk/simple-storage.js | 235 + addon-sdk/source/lib/sdk/stylesheet/style.js | 71 + addon-sdk/source/lib/sdk/stylesheet/utils.js | 75 + addon-sdk/source/lib/sdk/system.js | 172 + addon-sdk/source/lib/sdk/system/child_process.js | 332 ++ .../lib/sdk/system/child_process/subprocess.js | 186 + addon-sdk/source/lib/sdk/system/environment.js | 33 + addon-sdk/source/lib/sdk/system/events-shimmed.js | 16 + addon-sdk/source/lib/sdk/system/events.js | 181 + addon-sdk/source/lib/sdk/system/globals.js | 46 + addon-sdk/source/lib/sdk/system/process.js | 62 + addon-sdk/source/lib/sdk/system/runtime.js | 28 + addon-sdk/source/lib/sdk/system/unload.js | 104 + addon-sdk/source/lib/sdk/system/xul-app.js | 12 + addon-sdk/source/lib/sdk/system/xul-app.jsm | 242 + addon-sdk/source/lib/sdk/tab/events.js | 74 + addon-sdk/source/lib/sdk/tabs.js | 17 + addon-sdk/source/lib/sdk/tabs/common.js | 34 + addon-sdk/source/lib/sdk/tabs/events.js | 39 + addon-sdk/source/lib/sdk/tabs/helpers.js | 22 + addon-sdk/source/lib/sdk/tabs/namespace.js | 10 + addon-sdk/source/lib/sdk/tabs/observer.js | 113 + addon-sdk/source/lib/sdk/tabs/tab-fennec.js | 249 + addon-sdk/source/lib/sdk/tabs/tab-firefox.js | 353 ++ addon-sdk/source/lib/sdk/tabs/tab.js | 24 + addon-sdk/source/lib/sdk/tabs/tabs-firefox.js | 135 + addon-sdk/source/lib/sdk/tabs/utils.js | 370 ++ addon-sdk/source/lib/sdk/tabs/worker.js | 17 + addon-sdk/source/lib/sdk/test.js | 114 + addon-sdk/source/lib/sdk/test/assert.js | 366 ++ addon-sdk/source/lib/sdk/test/harness.js | 645 +++ addon-sdk/source/lib/sdk/test/httpd.js | 6 + addon-sdk/source/lib/sdk/test/loader.js | 123 + addon-sdk/source/lib/sdk/test/memory.js | 11 + addon-sdk/source/lib/sdk/test/options.js | 23 + addon-sdk/source/lib/sdk/test/runner.js | 131 + addon-sdk/source/lib/sdk/test/utils.js | 199 + addon-sdk/source/lib/sdk/timers.js | 105 + addon-sdk/source/lib/sdk/ui.js | 17 + addon-sdk/source/lib/sdk/ui/button/action.js | 114 + addon-sdk/source/lib/sdk/ui/button/contract.js | 73 + addon-sdk/source/lib/sdk/ui/button/toggle.js | 127 + addon-sdk/source/lib/sdk/ui/button/view.js | 243 + addon-sdk/source/lib/sdk/ui/button/view/events.js | 18 + addon-sdk/source/lib/sdk/ui/component.js | 182 + addon-sdk/source/lib/sdk/ui/frame.js | 16 + addon-sdk/source/lib/sdk/ui/frame/model.js | 154 + addon-sdk/source/lib/sdk/ui/frame/view.html | 18 + addon-sdk/source/lib/sdk/ui/frame/view.js | 150 + addon-sdk/source/lib/sdk/ui/id.js | 27 + addon-sdk/source/lib/sdk/ui/sidebar.js | 311 ++ addon-sdk/source/lib/sdk/ui/sidebar/actions.js | 10 + addon-sdk/source/lib/sdk/ui/sidebar/contract.js | 27 + addon-sdk/source/lib/sdk/ui/sidebar/namespace.js | 15 + addon-sdk/source/lib/sdk/ui/sidebar/utils.js | 8 + addon-sdk/source/lib/sdk/ui/sidebar/view.js | 214 + addon-sdk/source/lib/sdk/ui/state.js | 239 + addon-sdk/source/lib/sdk/ui/state/events.js | 18 + addon-sdk/source/lib/sdk/ui/toolbar.js | 16 + addon-sdk/source/lib/sdk/ui/toolbar/model.js | 151 + addon-sdk/source/lib/sdk/ui/toolbar/view.js | 248 + addon-sdk/source/lib/sdk/uri/resource.js | 37 + addon-sdk/source/lib/sdk/url.js | 349 ++ addon-sdk/source/lib/sdk/url/utils.js | 29 + addon-sdk/source/lib/sdk/util/array.js | 123 + addon-sdk/source/lib/sdk/util/collection.js | 115 + addon-sdk/source/lib/sdk/util/contract.js | 55 + addon-sdk/source/lib/sdk/util/deprecate.js | 40 + addon-sdk/source/lib/sdk/util/dispatcher.js | 54 + addon-sdk/source/lib/sdk/util/list.js | 90 + addon-sdk/source/lib/sdk/util/match-pattern.js | 113 + addon-sdk/source/lib/sdk/util/object.js | 104 + addon-sdk/source/lib/sdk/util/rules.js | 53 + addon-sdk/source/lib/sdk/util/sequence.js | 593 +++ addon-sdk/source/lib/sdk/util/uuid.js | 19 + addon-sdk/source/lib/sdk/view/core.js | 26 + addon-sdk/source/lib/sdk/webextension.js | 43 + addon-sdk/source/lib/sdk/window/browser.js | 54 + addon-sdk/source/lib/sdk/window/events.js | 68 + addon-sdk/source/lib/sdk/window/helpers.js | 81 + addon-sdk/source/lib/sdk/window/namespace.js | 6 + addon-sdk/source/lib/sdk/window/utils.js | 460 ++ addon-sdk/source/lib/sdk/windows.js | 32 + addon-sdk/source/lib/sdk/windows/fennec.js | 83 + addon-sdk/source/lib/sdk/windows/firefox.js | 224 + addon-sdk/source/lib/sdk/windows/observer.js | 53 + addon-sdk/source/lib/sdk/windows/tabs-fennec.js | 172 + addon-sdk/source/lib/sdk/worker/utils.js | 19 + addon-sdk/source/lib/sdk/zip/utils.js | 16 + addon-sdk/source/lib/test.js | 11 + addon-sdk/source/lib/toolkit/loader.js | 1147 +++++ addon-sdk/source/lib/toolkit/require.js | 91 + addon-sdk/source/mapping.json | 71 + addon-sdk/source/modules/system/Startup.js | 57 + addon-sdk/source/modules/system/moz.build | 9 + addon-sdk/source/package.json | 39 + addon-sdk/source/python-lib/cuddlefish/__init__.py | 959 ++++ addon-sdk/source/python-lib/cuddlefish/_version.py | 174 + addon-sdk/source/python-lib/cuddlefish/bunch.py | 34 + addon-sdk/source/python-lib/cuddlefish/manifest.py | 807 +++ .../cuddlefish/mobile-utils/bootstrap.js | 48 + .../python-lib/cuddlefish/mobile-utils/install.rdf | 39 + .../source/python-lib/cuddlefish/packaging.py | 463 ++ .../source/python-lib/cuddlefish/preflight.py | 77 + addon-sdk/source/python-lib/cuddlefish/prefs.py | 239 + .../python-lib/cuddlefish/property_parser.py | 111 + addon-sdk/source/python-lib/cuddlefish/rdf.py | 214 + addon-sdk/source/python-lib/cuddlefish/runner.py | 767 +++ .../source/python-lib/cuddlefish/templates.py | 32 + .../source/python-lib/cuddlefish/tests/__init__.py | 52 + .../packages/explicit-icon/explicit-icon.png | 0 .../packages/explicit-icon/explicit-icon64.png | 0 .../packages/explicit-icon/lib/main.js | 4 + .../packages/explicit-icon/package.json | 5 + .../packages/implicit-icon/icon.png | 0 .../packages/implicit-icon/icon64.png | 0 .../packages/implicit-icon/lib/main.js | 4 + .../packages/implicit-icon/package.json | 3 + .../bug-588119-files/packages/no-icon/lib/main.js | 4 + .../bug-588119-files/packages/no-icon/package.json | 3 + .../packages/bar/lib/bar-loader.js | 4 + .../bug-588661-files/packages/bar/package.json | 3 + .../packages/foo/lib/foo-loader.js | 4 + .../bug-588661-files/packages/foo/package.json | 4 + .../tests/bug-611495-files/jspath-one/docs/main.md | 5 + .../tests/bug-611495-files/jspath-one/lib/main.js | 8 + .../tests/bug-611495-files/jspath-one/package.json | 5 + .../packages/commonjs-naming/doc/foo.md | 5 + .../packages/commonjs-naming/lib/foo-loader.js | 5 + .../packages/commonjs-naming/package.json | 3 + .../packages/commonjs-naming/test/test-foo.js | 7 + .../packages/original-naming/docs/foo.md | 5 + .../packages/original-naming/lib/foo-loader.js | 5 + .../packages/original-naming/package.json | 3 + .../packages/original-naming/tests/test-foo.js | 7 + .../packages/default-lib/doc/foo.md | 5 + .../packages/default-lib/lib/foo.js | 5 + .../packages/default-lib/lib/loader.js | 5 + .../packages/default-lib/package.json | 3 + .../packages/default-lib/test/test-foo.js | 7 + .../packages/default-locale/locale/emptyFolder | 0 .../packages/default-locale/package.json | 1 + .../packages/default-root/doc/foo.md | 5 + .../bug-652227-files/packages/default-root/foo.js | 5 + .../packages/default-root/loader.js | 5 + .../packages/default-root/package.json | 3 + .../packages/default-root/test/test-foo.js | 7 + .../packages/explicit-dir-lib/alt-lib/foo.js | 5 + .../packages/explicit-dir-lib/alt-lib/loader.js | 5 + .../packages/explicit-dir-lib/doc/foo.md | 5 + .../packages/explicit-dir-lib/package.json | 4 + .../packages/explicit-dir-lib/test/test-foo.js | 7 + .../packages/explicit-lib/alt2-lib/foo.js | 5 + .../packages/explicit-lib/alt2-lib/loader.js | 5 + .../packages/explicit-lib/doc/foo.md | 5 + .../packages/explicit-lib/package.json | 4 + .../packages/explicit-lib/test/test-foo.js | 7 + .../packages/extra-options/docs/main.md | 5 + .../packages/extra-options/lib/main.js | 8 + .../packages/extra-options/package.json | 6 + .../tests/bug-715340-files/pkg-1-pack/package.json | 10 + .../bug-715340-files/pkg-2-unpack/package.json | 10 + .../tests/bug-715340-files/pkg-3-pack/package.json | 9 + .../tests/bug-906359-files/fullName/package.json | 9 + .../tests/bug-906359-files/none/package.json | 9 + .../tests/bug-906359-files/title/package.json | 9 + .../packages/foo/lib/bar-e10s-adapter.js | 11 + .../e10s-adapter-files/packages/foo/lib/bar.js | 5 + .../e10s-adapter-files/packages/foo/lib/foo.js | 5 + .../e10s-adapter-files/packages/foo/package.json | 1 + .../cuddlefish/tests/linker-files/five/lib/main.js | 5 + .../tests/linker-files/five/package.json | 3 + .../linker-files/four-deps/four-a/lib/misc.js | 5 + .../linker-files/four-deps/four-a/package.json | 4 + .../linker-files/four-deps/four-a/topfiles/main.js | 5 + .../cuddlefish/tests/linker-files/four/lib/main.js | 5 + .../tests/linker-files/four/package.json | 3 + .../cuddlefish/tests/linker-files/one/lib/main.js | 9 + .../tests/linker-files/one/lib/subdir/three.js | 6 + .../cuddlefish/tests/linker-files/one/lib/two.js | 8 + .../cuddlefish/tests/linker-files/one/package.json | 4 + .../tests/linker-files/seven/data/text.data | 1 + .../tests/linker-files/seven/lib/main.js | 6 + .../tests/linker-files/seven/lib/unused.js | 5 + .../tests/linker-files/seven/package.json | 4 + .../tests/linker-files/six/lib/unused.js | 5 + .../cuddlefish/tests/linker-files/six/package.json | 3 + .../tests/linker-files/six/unreachable.js | 5 + .../linker-files/three-deps/three-a/lib/main.js | 8 + .../three-deps/three-a/lib/subdir/subfile.js | 5 + .../linker-files/three-deps/three-a/lib/unused.js | 5 + .../three-deps/three-a/locale/fr-FR.properties | 5 + .../linker-files/three-deps/three-a/package.json | 3 + .../linker-files/three-deps/three-b/lib/main.js | 5 + .../three-deps/three-b/locale/fr-FR.properties | 6 + .../linker-files/three-deps/three-b/package.json | 3 + .../linker-files/three-deps/three-c/lib/main.js | 5 + .../linker-files/three-deps/three-c/lib/sub/foo.js | 6 + .../three-deps/three-c/locale/fr-FR.properties | 9 + .../linker-files/three-deps/three-c/package.json | 3 + .../tests/linker-files/three/data/msg.txt | 1 + .../linker-files/three/data/subdir/submsg.txt | 1 + .../tests/linker-files/three/lib/main.js | 8 + .../tests/linker-files/three/package.json | 3 + .../tests/linker-files/three/tests/nontest.js | 5 + .../tests/linker-files/three/tests/test-one.js | 5 + .../tests/linker-files/three/tests/test-two.js | 5 + .../static-files/packages/aardvark/doc/main.md | 0 .../static-files/packages/aardvark/lib/ignore_me | 3 + .../static-files/packages/aardvark/lib/main.js | 8 + .../aardvark/lib/surprise.js/ignore_me_too | 2 + .../static-files/packages/aardvark/package.json | 7 + .../packages/anteater_files/lib/main.js | 8 + .../packages/anteater_files/package.json | 8 + .../static-files/packages/api-utils/lib/loader.js | 7 + .../static-files/packages/api-utils/package.json | 5 + .../packages/barbeque/lib/bar-module.js | 7 + .../static-files/packages/barbeque/package.json | 4 + .../static-files/packages/minimal/lib/main.js | 8 + .../static-files/packages/minimal/package.json | 4 + .../packages/third_party/docs/third_party.md | 1 + .../packages/third_party/lib/third-party.js | 8 + .../static-files/packages/third_party/package.json | 7 + .../xpi-template/components/harness.js | 8 + .../python-lib/cuddlefish/tests/test_init.py | 211 + .../python-lib/cuddlefish/tests/test_licenses.py | 100 + .../python-lib/cuddlefish/tests/test_linker.py | 247 + .../python-lib/cuddlefish/tests/test_manifest.py | 257 + .../python-lib/cuddlefish/tests/test_packaging.py | 117 + .../python-lib/cuddlefish/tests/test_preflight.py | 147 + .../cuddlefish/tests/test_property_parser.py | 93 + .../source/python-lib/cuddlefish/tests/test_rdf.py | 54 + .../python-lib/cuddlefish/tests/test_runner.py | 27 + .../python-lib/cuddlefish/tests/test_util.py | 22 + .../python-lib/cuddlefish/tests/test_version.py | 28 + .../source/python-lib/cuddlefish/tests/test_xpi.py | 310 ++ addon-sdk/source/python-lib/cuddlefish/util.py | 23 + .../python-lib/cuddlefish/version_comparator.py | 206 + addon-sdk/source/python-lib/cuddlefish/xpi.py | 169 + addon-sdk/source/python-lib/jetpack_sdk_env.py | 66 + addon-sdk/source/python-lib/mozrunner/__init__.py | 694 +++ .../source/python-lib/mozrunner/killableprocess.py | 329 ++ addon-sdk/source/python-lib/mozrunner/qijo.py | 166 + .../source/python-lib/mozrunner/winprocess.py | 379 ++ addon-sdk/source/python-lib/mozrunner/wpk.py | 80 + .../source/python-lib/plural-rules-generator.py | 185 + addon-sdk/source/python-lib/simplejson/LICENSE.txt | 19 + addon-sdk/source/python-lib/simplejson/__init__.py | 376 ++ addon-sdk/source/python-lib/simplejson/decoder.py | 343 ++ addon-sdk/source/python-lib/simplejson/encoder.py | 395 ++ addon-sdk/source/python-lib/simplejson/scanner.py | 67 + addon-sdk/source/python-lib/simplejson/tool.py | 44 + .../source/test/addons/addon-manager/lib/main.js | 8 + .../test/addons/addon-manager/lib/test-main.js | 12 + .../source/test/addons/addon-manager/package.json | 7 + addon-sdk/source/test/addons/author-email/main.js | 14 + .../source/test/addons/author-email/package.json | 6 + .../source/test/addons/child_process/index.js | 39 + .../source/test/addons/child_process/package.json | 5 + .../source/test/addons/chrome/chrome.manifest | 5 + .../addons/chrome/chrome/content/new-window.xul | 4 + .../test/addons/chrome/chrome/content/panel.html | 10 + .../chrome/locale/en-US/description.properties | 1 + .../chrome/locale/ja-JP/description.properties | 1 + .../test/addons/chrome/chrome/skin/style.css | 4 + addon-sdk/source/test/addons/chrome/data/panel.js | 10 + addon-sdk/source/test/addons/chrome/main.js | 97 + addon-sdk/source/test/addons/chrome/package.json | 5 + .../test/addons/content-permissions/httpd.js | 5211 +++++++++++++++++++ .../source/test/addons/content-permissions/main.js | 89 + .../test/addons/content-permissions/package.json | 8 + .../content-script-messages-latency/httpd.js | 5211 +++++++++++++++++++ .../addons/content-script-messages-latency/main.js | 90 + .../content-script-messages-latency/package.json | 6 + addon-sdk/source/test/addons/contributors/main.js | 19 + .../source/test/addons/contributors/package.json | 6 + addon-sdk/source/test/addons/curly-id/lib/main.js | 29 + addon-sdk/source/test/addons/curly-id/package.json | 13 + addon-sdk/source/test/addons/developers/main.js | 19 + .../source/test/addons/developers/package.json | 8 + .../e10s-content/data/test-contentScriptFile.js | 5 + .../addons/e10s-content/data/test-page-worker.html | 13 + .../addons/e10s-content/data/test-page-worker.js | 29 + .../source/test/addons/e10s-content/data/test.html | 13 + .../test/addons/e10s-content/lib/fixtures.js | 8 + .../source/test/addons/e10s-content/lib/httpd.js | 5212 ++++++++++++++++++++ .../source/test/addons/e10s-content/lib/main.js | 22 + .../addons/e10s-content/lib/test-content-script.js | 845 ++++ .../addons/e10s-content/lib/test-content-worker.js | 1127 +++++ .../addons/e10s-content/lib/test-page-worker.js | 524 ++ .../source/test/addons/e10s-content/package.json | 11 + .../addons/e10s-l10n/data/test-localization.html | 29 + .../test/addons/e10s-l10n/locale/en.properties | 38 + .../test/addons/e10s-l10n/locale/eo.properties | 5 + .../test/addons/e10s-l10n/locale/fr-FR.properties | 14 + addon-sdk/source/test/addons/e10s-l10n/main.js | 289 ++ .../source/test/addons/e10s-l10n/package.json | 5 + addon-sdk/source/test/addons/e10s-remote/main.js | 578 +++ .../source/test/addons/e10s-remote/package.json | 9 + .../test/addons/e10s-remote/remote-module.js | 129 + addon-sdk/source/test/addons/e10s-remote/utils.js | 110 + addon-sdk/source/test/addons/e10s-tabs/lib/main.js | 23 + .../e10s-tabs/lib/private-browsing/helper.js | 91 + .../test/addons/e10s-tabs/lib/test-tab-events.js | 238 + .../test/addons/e10s-tabs/lib/test-tab-observer.js | 46 + .../test/addons/e10s-tabs/lib/test-tab-utils.js | 67 + .../source/test/addons/e10s-tabs/lib/test-tab.js | 87 + .../source/test/addons/e10s-tabs/package.json | 11 + addon-sdk/source/test/addons/e10s/lib/main.js | 65 + addon-sdk/source/test/addons/e10s/package.json | 10 + .../test/addons/embedded-webextension/main.js | 159 + .../test/addons/embedded-webextension/package.json | 6 + .../webextension/background-page.js | 10 + .../webextension/content-script.js | 10 + .../webextension/manifest.json | 18 + addon-sdk/source/test/addons/jetpack-addon.ini | 48 + .../l10n-properties/app-extension/application.ini | 11 + .../l10n-properties/app-extension/bootstrap.js | 339 ++ .../l10n-properties/app-extension/install.rdf | 33 + .../app-extension/locale/en-GB.properties | 28 + .../app-extension/locale/en-US.properties | 22 + .../app-extension/locale/eo.properties | 5 + .../app-extension/locale/fr-FR.properties | 14 + .../l10n-properties/data/test-localization.html | 24 + .../source/test/addons/l10n-properties/main.js | 202 + .../test/addons/l10n-properties/package.json | 6 + .../test/addons/l10n/data/test-localization.html | 29 + .../source/test/addons/l10n/locale/en.properties | 38 + .../source/test/addons/l10n/locale/eo.properties | 5 + .../test/addons/l10n/locale/fr-FR.properties | 14 + addon-sdk/source/test/addons/l10n/main.js | 289 ++ addon-sdk/source/test/addons/l10n/package.json | 5 + .../source/test/addons/layout-change/lib/main.js | 15 + .../layout-change/lib/test-cuddlefish-loader.js | 164 + .../layout-change/lib/test-toolkit-loader.js | 10 + .../source/test/addons/layout-change/package.json | 7 + addon-sdk/source/test/addons/main/main.js | 37 + addon-sdk/source/test/addons/main/package.json | 5 + .../test/addons/name-in-numbers-plus/index.js | 12 + .../test/addons/name-in-numbers-plus/package.json | 6 + .../source/test/addons/name-in-numbers/index.js | 12 + .../test/addons/name-in-numbers/package.json | 6 + addon-sdk/source/test/addons/packaging/main.js | 57 + .../source/test/addons/packaging/package.json | 6 + addon-sdk/source/test/addons/packed/main.js | 20 + addon-sdk/source/test/addons/packed/package.json | 6 + .../addons/page-mod-debugger-post/data/index.html | 11 + .../addons/page-mod-debugger-post/data/script.js | 16 + .../test/addons/page-mod-debugger-post/main.js | 136 + .../addons/page-mod-debugger-post/package.json | 6 + .../addons/page-mod-debugger-pre/data/index.html | 11 + .../addons/page-mod-debugger-pre/data/script.js | 16 + .../test/addons/page-mod-debugger-pre/main.js | 134 + .../test/addons/page-mod-debugger-pre/package.json | 6 + .../source/test/addons/page-worker/data/page.html | 9 + .../source/test/addons/page-worker/data/page.js | 13 + addon-sdk/source/test/addons/page-worker/main.js | 53 + .../source/test/addons/page-worker/package.json | 3 + .../test/addons/places/lib/favicon-helpers.js | 54 + addon-sdk/source/test/addons/places/lib/httpd.js | 5211 +++++++++++++++++++ addon-sdk/source/test/addons/places/lib/main.js | 27 + .../source/test/addons/places/lib/places-helper.js | 239 + .../addons/places/lib/test-places-bookmarks.js | 948 ++++ .../test/addons/places/lib/test-places-events.js | 328 ++ .../test/addons/places/lib/test-places-favicon.js | 242 + .../test/addons/places/lib/test-places-history.js | 244 + .../test/addons/places/lib/test-places-host.js | 301 ++ .../test/addons/places/lib/test-places-utils.js | 78 + addon-sdk/source/test/addons/places/package.json | 5 + .../test/addons/predefined-id-with-at/lib/main.js | 32 + .../test/addons/predefined-id-with-at/package.json | 13 + .../test/addons/preferences-branch/lib/main.js | 28 + .../test/addons/preferences-branch/package.json | 14 + .../test/addons/private-browsing-supported/main.js | 28 + .../addons/private-browsing-supported/package.json | 8 + .../private-browsing-supported/sidebar/utils.js | 67 + .../private-browsing-supported/test-page-mod.js | 119 + .../private-browsing-supported/test-panel.js | 99 + .../test-private-browsing.js | 111 + .../private-browsing-supported/test-selection.js | 447 ++ .../private-browsing-supported/test-sidebar.js | 212 + .../addons/private-browsing-supported/test-tabs.js | 34 + .../private-browsing-supported/test-window-tabs.js | 75 + .../private-browsing-supported/test-windows.js | 240 + addon-sdk/source/test/addons/remote/main.js | 578 +++ addon-sdk/source/test/addons/remote/package.json | 8 + .../source/test/addons/remote/remote-module.js | 129 + addon-sdk/source/test/addons/remote/utils.js | 110 + addon-sdk/source/test/addons/require/list.js | 6 + addon-sdk/source/test/addons/require/main.js | 87 + addon-sdk/source/test/addons/require/multiple/a.js | 5 + addon-sdk/source/test/addons/require/multiple/b.js | 5 + addon-sdk/source/test/addons/require/package.json | 8 + .../test/addons/require/packages/tabs/main.js | 5 + .../test/addons/require/packages/tabs/package.json | 3 + .../test/addons/require/packages/tabs/page-mod.js | 5 + .../source/test/addons/require/same-folder.js | 5 + .../test/addons/require/sub-folder/module.js | 5 + addon-sdk/source/test/addons/require/tabs.js | 5 + addon-sdk/source/test/addons/self/data/data.md | 1 + addon-sdk/source/test/addons/self/main.js | 23 + addon-sdk/source/test/addons/self/package.json | 5 + .../addons/simple-prefs-l10n/locale/en.properties | 5 + .../source/test/addons/simple-prefs-l10n/main.js | 65 + .../test/addons/simple-prefs-l10n/package.json | 10 + .../app-extension/application.ini | 11 + .../app-extension/bootstrap.js | 339 ++ .../app-extension/defaults/preferences/prefs.js | 7 + .../app-extension/install.rdf | 34 + .../app-extension/options.xul | 5 + .../addons/simple-prefs-regression/lib/main.js | 94 + .../addons/simple-prefs-regression/package.json | 24 + .../source/test/addons/simple-prefs/lib/main.js | 109 + .../source/test/addons/simple-prefs/package.json | 32 + .../source/test/addons/standard-id/lib/main.js | 30 + .../source/test/addons/standard-id/package.json | 13 + .../test/addons/tab-close-on-startup/main.js | 31 + .../test/addons/tab-close-on-startup/package.json | 5 + .../test/addons/toolkit-require-reload/main.js | 77 + .../addons/toolkit-require-reload/package.json | 5 + addon-sdk/source/test/addons/translators/main.js | 20 + .../source/test/addons/translators/package.json | 8 + .../test/addons/unsafe-content-script/main.js | 68 + .../test/addons/unsafe-content-script/package.json | 8 + addon-sdk/source/test/buffers/test-read-types.js | 368 ++ addon-sdk/source/test/buffers/test-write-types.js | 602 +++ .../source/test/commonjs-test-adapter/asserts.js | 54 + addon-sdk/source/test/context-menu/framescript.js | 44 + addon-sdk/source/test/context-menu/test-helper.js | 539 ++ addon-sdk/source/test/context-menu/util.js | 141 + addon-sdk/source/test/event/helpers.js | 112 + addon-sdk/source/test/fixtures.js | 41 + .../addon-install-unit-test@mozilla.com.xpi | Bin 0 -> 5670 bytes .../test/fixtures/addon-sdk/data/border-style.css | 4 + .../addon-sdk/data/test-contentScriptFile.js | 5 + .../fixtures/addon-sdk/data/test-page-worker.html | 13 + .../fixtures/addon-sdk/data/test-page-worker.js | 29 + .../source/test/fixtures/addon-sdk/data/test.html | 13 + addon-sdk/source/test/fixtures/addon/bootstrap.js | 9 + addon-sdk/source/test/fixtures/addon/index.js | 4 + addon-sdk/source/test/fixtures/addon/package.json | 5 + .../fixtures/bootstrap-addon/META-INF/manifest.mf | 17 + .../fixtures/bootstrap-addon/META-INF/mozilla.rsa | Bin 0 -> 4191 bytes .../fixtures/bootstrap-addon/META-INF/mozilla.sf | 4 + .../test/fixtures/bootstrap-addon/bootstrap.js | 10 + .../test/fixtures/bootstrap-addon/install.rdf | 27 + .../test/fixtures/bootstrap-addon/options.xul | 3 + addon-sdk/source/test/fixtures/bootstrap/utils.js | 52 + addon-sdk/source/test/fixtures/border-style.css | 4 + .../source/test/fixtures/child-process-scripts.js | 81 + .../fixtures/chrome-worker/addEventListener.js | 6 + .../source/test/fixtures/chrome-worker/jsctypes.js | 6 + .../source/test/fixtures/chrome-worker/onerror.js | 6 + .../test/fixtures/chrome-worker/onmessage.js | 8 + .../test/fixtures/chrome-worker/setTimeout.js | 8 + .../source/test/fixtures/chrome-worker/xhr.js | 11 + addon-sdk/source/test/fixtures/create_xpi.py | 15 + addon-sdk/source/test/fixtures/es5.js | 8 + addon-sdk/source/test/fixtures/include-file.css | 4 + addon-sdk/source/test/fixtures/index.html | 18 + .../source/test/fixtures/jsm-package/Test.jsm | 11 + .../source/test/fixtures/jsm-package/index.js | 46 + .../source/test/fixtures/jsm-package/package.json | 3 + addon-sdk/source/test/fixtures/loader/cycles/a.js | 7 + addon-sdk/source/test/fixtures/loader/cycles/b.js | 7 + addon-sdk/source/test/fixtures/loader/cycles/c.js | 7 + .../source/test/fixtures/loader/cycles/main.js | 14 + .../source/test/fixtures/loader/errors/boomer.js | 7 + .../source/test/fixtures/loader/errors/main.js | 9 + .../test/fixtures/loader/exceptions/boomer.js | 9 + .../source/test/fixtures/loader/exceptions/main.js | 11 + .../source/test/fixtures/loader/globals/main.js | 7 + .../source/test/fixtures/loader/json/invalid.json | 3 + .../source/test/fixtures/loader/json/manifest.json | 14 + .../source/test/fixtures/loader/json/mutation.json | 1 + .../test/fixtures/loader/json/nodotjson.json.js | 8 + .../source/test/fixtures/loader/json/test.json | 3 + .../source/test/fixtures/loader/json/test.json.js | 7 + addon-sdk/source/test/fixtures/loader/lazy/main.js | 9 + .../test/fixtures/loader/missing-twice/file.json | 1 + .../test/fixtures/loader/missing-twice/main.js | 32 + .../source/test/fixtures/loader/missing/main.js | 10 + addon-sdk/source/test/fixtures/loader/self/main.js | 8 + .../test/fixtures/loader/syntax-error/error.js | 11 + .../test/fixtures/loader/syntax-error/main.js | 10 + .../test/fixtures/loader/unsupported/fennec.js | 10 + .../test/fixtures/loader/unsupported/firefox.js | 10 + addon-sdk/source/test/fixtures/mofo_logo.SVG | 45 + addon-sdk/source/test/fixtures/moz.build | 22 + addon-sdk/source/test/fixtures/moz_favicon.ico | Bin 0 -> 1406 bytes .../test/fixtures/native-addon-test/dir/a.js | 5 + .../test/fixtures/native-addon-test/dir/a/index.js | 5 + .../test/fixtures/native-addon-test/dir/b.js | 6 + .../test/fixtures/native-addon-test/dir/c.js | 6 + .../test/fixtures/native-addon-test/dir/dummy.js | 6 + .../test/fixtures/native-addon-test/dir/test.jsm | 6 + .../fixtures/native-addon-test/expectedmap.json | 25 + .../test/fixtures/native-addon-test/index.js | 37 + .../fixtures/native-addon-test/newmodule/index.js | 5 + .../native-addon-test/newmodule/lib/file.js | 5 + .../native-addon-test/newmodule/package.json | 3 + .../test/fixtures/native-addon-test/package.json | 10 + .../test/fixtures/native-addon-test/utils/index.js | 7 + .../test/fixtures/native-overrides-test/ignore.js | 6 + .../test/fixtures/native-overrides-test/index.js | 19 + .../fixtures/native-overrides-test/lib/ignore.js | 6 + .../fixtures/native-overrides-test/lib/internal.js | 6 + .../fixtures/native-overrides-test/lib/tabs.js | 6 + .../fixtures/native-overrides-test/package.json | 18 + .../fixtures/preferences/curly-id/package.json | 14 + .../fixtures/preferences/no-prefs/package.json | 6 + .../preferences/preferences-branch/package.json | 14 + .../fixtures/preferences/simple-prefs/package.json | 75 + .../test/fixtures/sandbox-complex-character.js | 5 + addon-sdk/source/test/fixtures/sandbox-normal.js | 7 + .../test/fixtures/test-addon-extras-window.html | 21 + .../source/test/fixtures/test-addon-extras.html | 31 + .../source/test/fixtures/test-contentScriptFile.js | 5 + .../source/test/fixtures/test-context-menu.js | 5 + .../test/fixtures/test-iframe-postmessage.html | 23 + addon-sdk/source/test/fixtures/test-iframe.html | 12 + addon-sdk/source/test/fixtures/test-iframe.js | 16 + .../source/test/fixtures/test-message-manager.js | 6 + addon-sdk/source/test/fixtures/test-net-url.txt | 1 + addon-sdk/source/test/fixtures/test-page-mod.html | 12 + .../test/fixtures/test-sidebar-addon-global.html | 15 + .../test/fixtures/test-trusted-document.html | 20 + addon-sdk/source/test/fixtures/test.html | 25 + addon-sdk/source/test/fixtures/testLocalXhr.json | 1 + .../test/framescript-manager/frame-script.js | 13 + addon-sdk/source/test/framescript-manager/pong.js | 7 + .../source/test/framescript-util/frame-script.js | 21 + addon-sdk/source/test/jetpack-package.ini | 179 + addon-sdk/source/test/leak/jetpack-package.ini | 7 + addon-sdk/source/test/leak/leak-utils.js | 80 + .../test/leak/test-leak-event-dom-closed-window.js | 29 + addon-sdk/source/test/leak/test-leak-tab-events.js | 46 + .../source/test/leak/test-leak-window-events.js | 65 + addon-sdk/source/test/lib/httpd.js | 5212 ++++++++++++++++++++ addon-sdk/source/test/loader/b2g.js | 41 + addon-sdk/source/test/loader/fixture.js | 7 + addon-sdk/source/test/loader/user-global.js | 11 + addon-sdk/source/test/modules/add.js | 9 + addon-sdk/source/test/modules/async1.js | 14 + addon-sdk/source/test/modules/async2.js | 8 + .../source/test/modules/badExportAndReturn.js | 10 + addon-sdk/source/test/modules/badFirst.js | 9 + addon-sdk/source/test/modules/badSecond.js | 8 + addon-sdk/source/test/modules/blue.js | 9 + addon-sdk/source/test/modules/castor.js | 10 + addon-sdk/source/test/modules/cheetah.js | 9 + addon-sdk/source/test/modules/color.js | 7 + addon-sdk/source/test/modules/dupe.js | 15 + addon-sdk/source/test/modules/dupeNested.js | 15 + addon-sdk/source/test/modules/dupeSetExports.js | 8 + addon-sdk/source/test/modules/exportsEquals.js | 5 + addon-sdk/source/test/modules/green.js | 10 + addon-sdk/source/test/modules/lion.js | 7 + addon-sdk/source/test/modules/orange.js | 10 + addon-sdk/source/test/modules/pollux.js | 10 + addon-sdk/source/test/modules/red.js | 16 + addon-sdk/source/test/modules/setExports.js | 5 + addon-sdk/source/test/modules/subtract.js | 9 + addon-sdk/source/test/modules/tiger.js | 8 + addon-sdk/source/test/modules/traditional1.js | 12 + addon-sdk/source/test/modules/traditional2.js | 6 + addon-sdk/source/test/modules/types/cat.js | 5 + addon-sdk/source/test/page-mod/helpers.js | 117 + addon-sdk/source/test/path/test-path.js | 430 ++ addon-sdk/source/test/preferences/common.json | 16 + addon-sdk/source/test/preferences/e10s-off.json | 5 + addon-sdk/source/test/preferences/e10s-on.json | 3 + addon-sdk/source/test/preferences/firefox.json | 11 + .../source/test/preferences/no-connections.json | 41 + .../test/preferences/test-e10s-preferences.js | 15 + .../source/test/preferences/test-preferences.js | 15 + addon-sdk/source/test/preferences/test.json | 46 + addon-sdk/source/test/private-browsing/helper.js | 58 + addon-sdk/source/test/private-browsing/tabs.js | 25 + addon-sdk/source/test/private-browsing/windows.js | 115 + .../source/test/querystring/test-querystring.js | 205 + addon-sdk/source/test/sidebar/utils.js | 74 + addon-sdk/source/test/tabs/test-fennec-tabs.js | 595 +++ addon-sdk/source/test/tabs/test-firefox-tabs.js | 1305 +++++ addon-sdk/source/test/tabs/utils.js | 24 + addon-sdk/source/test/test-addon-bootstrap.js | 97 + addon-sdk/source/test/test-addon-extras.js | 70 + addon-sdk/source/test/test-addon-installer.js | 230 + addon-sdk/source/test/test-addon-window.js | 22 + addon-sdk/source/test/test-api-utils.js | 316 ++ addon-sdk/source/test/test-array.js | 103 + addon-sdk/source/test/test-base64.js | 100 + addon-sdk/source/test/test-bootstrap.js | 19 + addon-sdk/source/test/test-browser-events.js | 102 + addon-sdk/source/test/test-buffer.js | 563 +++ addon-sdk/source/test/test-byte-streams.js | 169 + addon-sdk/source/test/test-child_process.js | 545 ++ addon-sdk/source/test/test-chrome.js | 84 + addon-sdk/source/test/test-clipboard.js | 170 + addon-sdk/source/test/test-collection.js | 128 + .../source/test/test-commonjs-test-adapter.js | 11 + addon-sdk/source/test/test-content-events.js | 92 + addon-sdk/source/test/test-content-script.js | 845 ++++ addon-sdk/source/test/test-content-sync-worker.js | 965 ++++ addon-sdk/source/test/test-content-worker.js | 1129 +++++ addon-sdk/source/test/test-context-menu.html | 98 + addon-sdk/source/test/test-context-menu.js | 3763 ++++++++++++++ addon-sdk/source/test/test-context-menu@2.js | 1350 +++++ addon-sdk/source/test/test-cuddlefish.js | 78 + addon-sdk/source/test/test-deprecate.js | 160 + addon-sdk/source/test/test-dev-panel.js | 426 ++ addon-sdk/source/test/test-diffpatcher.js | 8 + addon-sdk/source/test/test-dispatcher.js | 76 + addon-sdk/source/test/test-disposable.js | 393 ++ addon-sdk/source/test/test-dom.js | 88 + addon-sdk/source/test/test-environment.js | 49 + addon-sdk/source/test/test-event-core.js | 347 ++ addon-sdk/source/test/test-event-dom.js | 92 + addon-sdk/source/test/test-event-target.js | 222 + addon-sdk/source/test/test-event-utils.js | 285 ++ addon-sdk/source/test/test-file.js | 271 + addon-sdk/source/test/test-frame-utils.js | 59 + addon-sdk/source/test/test-framescript-manager.js | 32 + addon-sdk/source/test/test-framescript-util.js | 45 + addon-sdk/source/test/test-fs.js | 621 +++ addon-sdk/source/test/test-functional.js | 463 ++ addon-sdk/source/test/test-globals.js | 30 + addon-sdk/source/test/test-heritage.js | 301 ++ addon-sdk/source/test/test-hidden-frame.js | 71 + addon-sdk/source/test/test-host-events.js | 99 + addon-sdk/source/test/test-hotkeys.js | 183 + addon-sdk/source/test/test-httpd.js | 73 + addon-sdk/source/test/test-indexed-db.js | 182 + addon-sdk/source/test/test-jetpack-id.js | 64 + addon-sdk/source/test/test-keyboard-observer.js | 36 + addon-sdk/source/test/test-keyboard-utils.js | 61 + addon-sdk/source/test/test-l10n-locale.js | 169 + addon-sdk/source/test/test-l10n-plural-rules.js | 85 + addon-sdk/source/test/test-lang-type.js | 166 + addon-sdk/source/test/test-libxul.js | 18 + addon-sdk/source/test/test-list.js | 58 + addon-sdk/source/test/test-loader.js | 657 +++ addon-sdk/source/test/test-match-pattern.js | 137 + addon-sdk/source/test/test-method.js | 7 + addon-sdk/source/test/test-module.js | 36 + addon-sdk/source/test/test-modules.js | 150 + .../source/test/test-mozilla-toolkit-versioning.js | 59 + addon-sdk/source/test/test-mpl2-license-header.js | 105 + addon-sdk/source/test/test-namespace.js | 120 + addon-sdk/source/test/test-native-loader.js | 423 ++ addon-sdk/source/test/test-native-options.js | 183 + addon-sdk/source/test/test-net-url.js | 137 + addon-sdk/source/test/test-node-os.js | 33 + addon-sdk/source/test/test-notifications.js | 94 + addon-sdk/source/test/test-object.js | 36 + addon-sdk/source/test/test-observers.js | 183 + addon-sdk/source/test/test-page-mod-debug.js | 66 + addon-sdk/source/test/test-page-mod.js | 2214 +++++++++ addon-sdk/source/test/test-page-worker.js | 558 +++ addon-sdk/source/test/test-panel.js | 1426 ++++++ addon-sdk/source/test/test-passwords-utils.js | 141 + addon-sdk/source/test/test-passwords.js | 280 ++ addon-sdk/source/test/test-path.js | 6 + addon-sdk/source/test/test-plain-text-console.js | 278 ++ addon-sdk/source/test/test-preferences-service.js | 155 + addon-sdk/source/test/test-preferences-target.js | 42 + addon-sdk/source/test/test-private-browsing.js | 88 + addon-sdk/source/test/test-promise.js | 461 ++ addon-sdk/source/test/test-querystring.js | 6 + addon-sdk/source/test/test-reference.js | 99 + addon-sdk/source/test/test-request.js | 548 ++ addon-sdk/source/test/test-require.js | 67 + addon-sdk/source/test/test-rules.js | 79 + addon-sdk/source/test/test-sandbox.js | 161 + addon-sdk/source/test/test-selection.js | 985 ++++ addon-sdk/source/test/test-self.js | 79 + addon-sdk/source/test/test-sequence.js | 1245 +++++ addon-sdk/source/test/test-set-exports.js | 37 + addon-sdk/source/test/test-shared-require.js | 36 + addon-sdk/source/test/test-simple-prefs.js | 331 ++ addon-sdk/source/test/test-simple-storage.js | 322 ++ addon-sdk/source/test/test-system-events.js | 278 ++ addon-sdk/source/test/test-system-input-output.js | 319 ++ addon-sdk/source/test/test-system-runtime.js | 25 + addon-sdk/source/test/test-system-startup.js | 19 + addon-sdk/source/test/test-system.js | 37 + addon-sdk/source/test/test-tab-events.js | 238 + addon-sdk/source/test/test-tab-observer.js | 45 + addon-sdk/source/test/test-tab-utils.js | 69 + addon-sdk/source/test/test-tab.js | 228 + addon-sdk/source/test/test-tabs-common.js | 654 +++ addon-sdk/source/test/test-tabs.js | 20 + addon-sdk/source/test/test-test-addon-file.js | 16 + addon-sdk/source/test/test-test-assert.js | 218 + addon-sdk/source/test/test-test-loader.js | 59 + addon-sdk/source/test/test-test-memory.js | 25 + addon-sdk/source/test/test-test-utils-async.js | 86 + addon-sdk/source/test/test-test-utils-generator.js | 76 + addon-sdk/source/test/test-test-utils-sync.js | 84 + addon-sdk/source/test/test-test-utils.js | 81 + addon-sdk/source/test/test-text-streams.js | 154 + addon-sdk/source/test/test-timer.js | 229 + addon-sdk/source/test/test-traceback.js | 139 + addon-sdk/source/test/test-ui-action-button.js | 1182 +++++ addon-sdk/source/test/test-ui-frame.js | 252 + addon-sdk/source/test/test-ui-id.js | 43 + .../test/test-ui-sidebar-private-browsing.js | 203 + addon-sdk/source/test/test-ui-sidebar.js | 1579 ++++++ addon-sdk/source/test/test-ui-toggle-button.js | 1386 ++++++ addon-sdk/source/test/test-ui-toolbar.js | 511 ++ addon-sdk/source/test/test-unit-test-finder.js | 57 + addon-sdk/source/test/test-unit-test.js | 270 + addon-sdk/source/test/test-unload.js | 71 + addon-sdk/source/test/test-unsupported-skip.js | 24 + addon-sdk/source/test/test-uri-resource.js | 43 + addon-sdk/source/test/test-url.js | 502 ++ addon-sdk/source/test/test-uuid.js | 26 + addon-sdk/source/test/test-weak-set.js | 146 + addon-sdk/source/test/test-window-events.js | 63 + addon-sdk/source/test/test-window-observer.js | 61 + .../test/test-window-utils-private-browsing.js | 210 + addon-sdk/source/test/test-window-utils.js | 266 + addon-sdk/source/test/test-window-utils2.js | 112 + addon-sdk/source/test/test-windows-common.js | 104 + addon-sdk/source/test/test-windows.js | 24 + addon-sdk/source/test/test-xhr.js | 89 + addon-sdk/source/test/test-xpcom.js | 232 + addon-sdk/source/test/test-xul-app.js | 145 + addon-sdk/source/test/traits/assert.js | 101 + addon-sdk/source/test/traits/utils.js | 56 + addon-sdk/source/test/util.js | 90 + .../source/test/windows/test-fennec-windows.js | 48 + .../source/test/windows/test-firefox-windows.js | 621 +++ addon-sdk/source/test/zip/utils.js | 126 + 972 files changed, 138607 insertions(+) create mode 100644 addon-sdk/source/.gitattributes create mode 100644 addon-sdk/source/.gitignore create mode 100644 addon-sdk/source/.jpmignore create mode 100644 addon-sdk/source/.travis.yml create mode 100644 addon-sdk/source/CONTRIBUTING.md create mode 100644 addon-sdk/source/LICENSE create mode 100644 addon-sdk/source/README.md create mode 100644 addon-sdk/source/app-extension/application.ini create mode 100644 addon-sdk/source/app-extension/bootstrap.js create mode 100644 addon-sdk/source/app-extension/install.rdf create mode 100644 addon-sdk/source/bin/activate create mode 100644 addon-sdk/source/bin/activate.bat create mode 100644 addon-sdk/source/bin/activate.fish create mode 100644 addon-sdk/source/bin/activate.ps1 create mode 100755 addon-sdk/source/bin/cfx create mode 100644 addon-sdk/source/bin/cfx.bat create mode 100644 addon-sdk/source/bin/deactivate.bat create mode 100644 addon-sdk/source/bin/fx-download.sh create mode 100755 addon-sdk/source/bin/integration-scripts/buildbot-run-cfx-helper create mode 100644 addon-sdk/source/bin/integration-scripts/integration-check create mode 100644 addon-sdk/source/bin/jpm-test.js create mode 100644 addon-sdk/source/bin/node-scripts/apply-patch.js create mode 100644 addon-sdk/source/bin/node-scripts/test.addons.js create mode 100644 addon-sdk/source/bin/node-scripts/test.docs.js create mode 100644 addon-sdk/source/bin/node-scripts/test.examples.js create mode 100644 addon-sdk/source/bin/node-scripts/test.firefox-bin.js create mode 100644 addon-sdk/source/bin/node-scripts/test.ini.js create mode 100644 addon-sdk/source/bin/node-scripts/test.modules.js create mode 100644 addon-sdk/source/bin/node-scripts/update-ini.js create mode 100644 addon-sdk/source/bin/node-scripts/utils.js create mode 100644 addon-sdk/source/bin/node-scripts/words.txt create mode 100644 addon-sdk/source/examples/actor-repl/README.md create mode 100644 addon-sdk/source/examples/actor-repl/data/codemirror-compressed.js create mode 100644 addon-sdk/source/examples/actor-repl/data/codemirror.css create mode 100644 addon-sdk/source/examples/actor-repl/data/index.html create mode 100644 addon-sdk/source/examples/actor-repl/data/main.css create mode 100644 addon-sdk/source/examples/actor-repl/data/robot.png create mode 100644 addon-sdk/source/examples/actor-repl/index.js create mode 100644 addon-sdk/source/examples/actor-repl/package.json create mode 100644 addon-sdk/source/examples/actor-repl/test/test-main.js create mode 100644 addon-sdk/source/examples/debug-client/data/client.js create mode 100644 addon-sdk/source/examples/debug-client/data/index.html create mode 100644 addon-sdk/source/examples/debug-client/data/plugin.png create mode 100644 addon-sdk/source/examples/debug-client/data/task.js create mode 100644 addon-sdk/source/examples/debug-client/index.js create mode 100644 addon-sdk/source/examples/debug-client/package.json create mode 100644 addon-sdk/source/examples/debug-client/test/test-main.js create mode 100644 addon-sdk/source/examples/reading-data/data/mom.png create mode 100644 addon-sdk/source/examples/reading-data/data/sample.html create mode 100644 addon-sdk/source/examples/reading-data/lib/main.js create mode 100644 addon-sdk/source/examples/reading-data/package.json create mode 100644 addon-sdk/source/examples/reading-data/tests/test-main.js create mode 100644 addon-sdk/source/examples/theme/data/icon-16.png create mode 100644 addon-sdk/source/examples/theme/data/index.html create mode 100644 addon-sdk/source/examples/theme/data/theme.css create mode 100644 addon-sdk/source/examples/theme/lib/main.js create mode 100644 addon-sdk/source/examples/theme/package.json create mode 100644 addon-sdk/source/examples/theme/test/test-main.js create mode 100644 addon-sdk/source/examples/toolbar-api/data/favicon.ico create mode 100644 addon-sdk/source/examples/toolbar-api/data/index.html create mode 100644 addon-sdk/source/examples/toolbar-api/lib/main.js create mode 100644 addon-sdk/source/examples/toolbar-api/package.json create mode 100644 addon-sdk/source/examples/toolbar-api/test/test-main.js create mode 100644 addon-sdk/source/examples/ui-button-apis/lib/main.js create mode 100644 addon-sdk/source/examples/ui-button-apis/package.json create mode 100644 addon-sdk/source/examples/ui-button-apis/tests/test-main.js create mode 100644 addon-sdk/source/gulpfile.js create mode 100644 addon-sdk/source/lib/dev/debuggee.js create mode 100644 addon-sdk/source/lib/dev/frame-script.js create mode 100644 addon-sdk/source/lib/dev/panel.js create mode 100644 addon-sdk/source/lib/dev/panel/view.js create mode 100644 addon-sdk/source/lib/dev/ports.js create mode 100644 addon-sdk/source/lib/dev/theme.js create mode 100644 addon-sdk/source/lib/dev/theme/hooks.js create mode 100644 addon-sdk/source/lib/dev/toolbox.js create mode 100644 addon-sdk/source/lib/dev/utils.js create mode 100644 addon-sdk/source/lib/dev/volcan.js create mode 100644 addon-sdk/source/lib/diffpatcher/.travis.yml create mode 100644 addon-sdk/source/lib/diffpatcher/History.md create mode 100644 addon-sdk/source/lib/diffpatcher/License.md create mode 100644 addon-sdk/source/lib/diffpatcher/Readme.md create mode 100644 addon-sdk/source/lib/diffpatcher/diff.js create mode 100644 addon-sdk/source/lib/diffpatcher/index.js create mode 100644 addon-sdk/source/lib/diffpatcher/package.json create mode 100644 addon-sdk/source/lib/diffpatcher/patch.js create mode 100644 addon-sdk/source/lib/diffpatcher/rebase.js create mode 100644 addon-sdk/source/lib/diffpatcher/test/common.js create mode 100644 addon-sdk/source/lib/diffpatcher/test/diff.js create mode 100644 addon-sdk/source/lib/diffpatcher/test/index.js create mode 100644 addon-sdk/source/lib/diffpatcher/test/patch.js create mode 100644 addon-sdk/source/lib/diffpatcher/test/tap.js create mode 100644 addon-sdk/source/lib/framescript/FrameScriptManager.jsm create mode 100644 addon-sdk/source/lib/framescript/content.jsm create mode 100644 addon-sdk/source/lib/framescript/context-menu.js create mode 100644 addon-sdk/source/lib/framescript/manager.js create mode 100644 addon-sdk/source/lib/framescript/util.js create mode 100644 addon-sdk/source/lib/index.js create mode 100644 addon-sdk/source/lib/jetpack-id/index.js create mode 100644 addon-sdk/source/lib/jetpack-id/package.json create mode 100644 addon-sdk/source/lib/method/.travis.yml create mode 100644 addon-sdk/source/lib/method/History.md create mode 100644 addon-sdk/source/lib/method/License.md create mode 100644 addon-sdk/source/lib/method/Readme.md create mode 100644 addon-sdk/source/lib/method/core.js create mode 100644 addon-sdk/source/lib/method/package.json create mode 100644 addon-sdk/source/lib/method/test/browser.js create mode 100644 addon-sdk/source/lib/method/test/common.js create mode 100644 addon-sdk/source/lib/mozilla-toolkit-versioning/index.js create mode 100644 addon-sdk/source/lib/mozilla-toolkit-versioning/lib/utils.js create mode 100644 addon-sdk/source/lib/mozilla-toolkit-versioning/package.json create mode 100644 addon-sdk/source/lib/node/os.js create mode 100644 addon-sdk/source/lib/sdk/addon/bootstrap.js create mode 100644 addon-sdk/source/lib/sdk/addon/events.js create mode 100644 addon-sdk/source/lib/sdk/addon/host.js create mode 100644 addon-sdk/source/lib/sdk/addon/installer.js create mode 100644 addon-sdk/source/lib/sdk/addon/manager.js create mode 100644 addon-sdk/source/lib/sdk/addon/runner.js create mode 100644 addon-sdk/source/lib/sdk/addon/window.js create mode 100644 addon-sdk/source/lib/sdk/base64.js create mode 100644 addon-sdk/source/lib/sdk/browser/events.js create mode 100644 addon-sdk/source/lib/sdk/clipboard.js create mode 100644 addon-sdk/source/lib/sdk/console/plain-text.js create mode 100644 addon-sdk/source/lib/sdk/console/traceback.js create mode 100644 addon-sdk/source/lib/sdk/content/content-worker.js create mode 100644 addon-sdk/source/lib/sdk/content/content.js create mode 100644 addon-sdk/source/lib/sdk/content/context-menu.js create mode 100644 addon-sdk/source/lib/sdk/content/events.js create mode 100644 addon-sdk/source/lib/sdk/content/l10n-html.js create mode 100644 addon-sdk/source/lib/sdk/content/loader.js create mode 100644 addon-sdk/source/lib/sdk/content/mod.js create mode 100644 addon-sdk/source/lib/sdk/content/page-mod.js create mode 100644 addon-sdk/source/lib/sdk/content/page-worker.js create mode 100644 addon-sdk/source/lib/sdk/content/sandbox.js create mode 100644 addon-sdk/source/lib/sdk/content/sandbox/events.js create mode 100644 addon-sdk/source/lib/sdk/content/tab-events.js create mode 100644 addon-sdk/source/lib/sdk/content/thumbnail.js create mode 100644 addon-sdk/source/lib/sdk/content/utils.js create mode 100644 addon-sdk/source/lib/sdk/content/worker-child.js create mode 100644 addon-sdk/source/lib/sdk/content/worker.js create mode 100644 addon-sdk/source/lib/sdk/context-menu.js create mode 100644 addon-sdk/source/lib/sdk/context-menu/context.js create mode 100644 addon-sdk/source/lib/sdk/context-menu/core.js create mode 100644 addon-sdk/source/lib/sdk/context-menu/readers.js create mode 100644 addon-sdk/source/lib/sdk/context-menu@2.js create mode 100644 addon-sdk/source/lib/sdk/core/disposable.js create mode 100644 addon-sdk/source/lib/sdk/core/heritage.js create mode 100644 addon-sdk/source/lib/sdk/core/namespace.js create mode 100644 addon-sdk/source/lib/sdk/core/observer.js create mode 100644 addon-sdk/source/lib/sdk/core/promise.js create mode 100644 addon-sdk/source/lib/sdk/core/reference.js create mode 100644 addon-sdk/source/lib/sdk/deprecated/api-utils.js create mode 100644 addon-sdk/source/lib/sdk/deprecated/events/assembler.js create mode 100644 addon-sdk/source/lib/sdk/deprecated/sync-worker.js create mode 100644 addon-sdk/source/lib/sdk/deprecated/unit-test-finder.js create mode 100644 addon-sdk/source/lib/sdk/deprecated/unit-test.js create mode 100644 addon-sdk/source/lib/sdk/deprecated/window-utils.js create mode 100644 addon-sdk/source/lib/sdk/dom/events-shimmed.js create mode 100644 addon-sdk/source/lib/sdk/dom/events.js create mode 100644 addon-sdk/source/lib/sdk/dom/events/keys.js create mode 100644 addon-sdk/source/lib/sdk/event/chrome.js create mode 100644 addon-sdk/source/lib/sdk/event/core.js create mode 100644 addon-sdk/source/lib/sdk/event/dom.js create mode 100644 addon-sdk/source/lib/sdk/event/target.js create mode 100644 addon-sdk/source/lib/sdk/event/utils.js create mode 100644 addon-sdk/source/lib/sdk/frame/hidden-frame.js create mode 100644 addon-sdk/source/lib/sdk/frame/utils.js create mode 100644 addon-sdk/source/lib/sdk/fs/path.js create mode 100644 addon-sdk/source/lib/sdk/hotkeys.js create mode 100644 addon-sdk/source/lib/sdk/indexed-db.js create mode 100644 addon-sdk/source/lib/sdk/input/browser.js create mode 100644 addon-sdk/source/lib/sdk/input/customizable-ui.js create mode 100644 addon-sdk/source/lib/sdk/input/frame.js create mode 100644 addon-sdk/source/lib/sdk/input/system.js create mode 100644 addon-sdk/source/lib/sdk/io/buffer.js create mode 100644 addon-sdk/source/lib/sdk/io/byte-streams.js create mode 100644 addon-sdk/source/lib/sdk/io/file.js create mode 100644 addon-sdk/source/lib/sdk/io/fs.js create mode 100644 addon-sdk/source/lib/sdk/io/stream.js create mode 100644 addon-sdk/source/lib/sdk/io/text-streams.js create mode 100644 addon-sdk/source/lib/sdk/keyboard/hotkeys.js create mode 100644 addon-sdk/source/lib/sdk/keyboard/observer.js create mode 100644 addon-sdk/source/lib/sdk/keyboard/utils.js create mode 100644 addon-sdk/source/lib/sdk/l10n.js create mode 100644 addon-sdk/source/lib/sdk/l10n/core.js create mode 100644 addon-sdk/source/lib/sdk/l10n/html.js create mode 100644 addon-sdk/source/lib/sdk/l10n/json/core.js create mode 100644 addon-sdk/source/lib/sdk/l10n/loader.js create mode 100644 addon-sdk/source/lib/sdk/l10n/locale.js create mode 100644 addon-sdk/source/lib/sdk/l10n/plural-rules.js create mode 100644 addon-sdk/source/lib/sdk/l10n/prefs.js create mode 100644 addon-sdk/source/lib/sdk/l10n/properties/core.js create mode 100644 addon-sdk/source/lib/sdk/lang/functional.js create mode 100644 addon-sdk/source/lib/sdk/lang/functional/concurrent.js create mode 100644 addon-sdk/source/lib/sdk/lang/functional/core.js create mode 100644 addon-sdk/source/lib/sdk/lang/functional/helpers.js create mode 100644 addon-sdk/source/lib/sdk/lang/type.js create mode 100644 addon-sdk/source/lib/sdk/lang/weak-set.js create mode 100644 addon-sdk/source/lib/sdk/loader/cuddlefish.js create mode 100644 addon-sdk/source/lib/sdk/loader/sandbox.js create mode 100644 addon-sdk/source/lib/sdk/messaging.js create mode 100644 addon-sdk/source/lib/sdk/model/core.js create mode 100644 addon-sdk/source/lib/sdk/net/url.js create mode 100644 addon-sdk/source/lib/sdk/net/xhr.js create mode 100644 addon-sdk/source/lib/sdk/notifications.js create mode 100644 addon-sdk/source/lib/sdk/output/system.js create mode 100644 addon-sdk/source/lib/sdk/page-mod.js create mode 100644 addon-sdk/source/lib/sdk/page-mod/match-pattern.js create mode 100644 addon-sdk/source/lib/sdk/page-worker.js create mode 100644 addon-sdk/source/lib/sdk/panel.js create mode 100644 addon-sdk/source/lib/sdk/panel/events.js create mode 100644 addon-sdk/source/lib/sdk/panel/utils.js create mode 100644 addon-sdk/source/lib/sdk/passwords.js create mode 100644 addon-sdk/source/lib/sdk/passwords/utils.js create mode 100644 addon-sdk/source/lib/sdk/places/bookmarks.js create mode 100644 addon-sdk/source/lib/sdk/places/contract.js create mode 100644 addon-sdk/source/lib/sdk/places/events.js create mode 100644 addon-sdk/source/lib/sdk/places/favicon.js create mode 100644 addon-sdk/source/lib/sdk/places/history.js create mode 100644 addon-sdk/source/lib/sdk/places/host/host-bookmarks.js create mode 100644 addon-sdk/source/lib/sdk/places/host/host-query.js create mode 100644 addon-sdk/source/lib/sdk/places/host/host-tags.js create mode 100644 addon-sdk/source/lib/sdk/places/utils.js create mode 100644 addon-sdk/source/lib/sdk/platform/xpcom.js create mode 100644 addon-sdk/source/lib/sdk/preferences/event-target.js create mode 100644 addon-sdk/source/lib/sdk/preferences/native-options.js create mode 100644 addon-sdk/source/lib/sdk/preferences/service.js create mode 100644 addon-sdk/source/lib/sdk/preferences/utils.js create mode 100644 addon-sdk/source/lib/sdk/private-browsing.js create mode 100644 addon-sdk/source/lib/sdk/private-browsing/utils.js create mode 100644 addon-sdk/source/lib/sdk/querystring.js create mode 100644 addon-sdk/source/lib/sdk/remote/child.js create mode 100644 addon-sdk/source/lib/sdk/remote/core.js create mode 100644 addon-sdk/source/lib/sdk/remote/parent.js create mode 100644 addon-sdk/source/lib/sdk/remote/utils.js create mode 100644 addon-sdk/source/lib/sdk/request.js create mode 100644 addon-sdk/source/lib/sdk/selection.js create mode 100644 addon-sdk/source/lib/sdk/self.js create mode 100644 addon-sdk/source/lib/sdk/simple-prefs.js create mode 100644 addon-sdk/source/lib/sdk/simple-storage.js create mode 100644 addon-sdk/source/lib/sdk/stylesheet/style.js create mode 100644 addon-sdk/source/lib/sdk/stylesheet/utils.js create mode 100644 addon-sdk/source/lib/sdk/system.js create mode 100644 addon-sdk/source/lib/sdk/system/child_process.js create mode 100644 addon-sdk/source/lib/sdk/system/child_process/subprocess.js create mode 100644 addon-sdk/source/lib/sdk/system/environment.js create mode 100644 addon-sdk/source/lib/sdk/system/events-shimmed.js create mode 100644 addon-sdk/source/lib/sdk/system/events.js create mode 100644 addon-sdk/source/lib/sdk/system/globals.js create mode 100644 addon-sdk/source/lib/sdk/system/process.js create mode 100644 addon-sdk/source/lib/sdk/system/runtime.js create mode 100644 addon-sdk/source/lib/sdk/system/unload.js create mode 100644 addon-sdk/source/lib/sdk/system/xul-app.js create mode 100644 addon-sdk/source/lib/sdk/system/xul-app.jsm create mode 100644 addon-sdk/source/lib/sdk/tab/events.js create mode 100644 addon-sdk/source/lib/sdk/tabs.js create mode 100644 addon-sdk/source/lib/sdk/tabs/common.js create mode 100644 addon-sdk/source/lib/sdk/tabs/events.js create mode 100644 addon-sdk/source/lib/sdk/tabs/helpers.js create mode 100644 addon-sdk/source/lib/sdk/tabs/namespace.js create mode 100644 addon-sdk/source/lib/sdk/tabs/observer.js create mode 100644 addon-sdk/source/lib/sdk/tabs/tab-fennec.js create mode 100644 addon-sdk/source/lib/sdk/tabs/tab-firefox.js create mode 100644 addon-sdk/source/lib/sdk/tabs/tab.js create mode 100644 addon-sdk/source/lib/sdk/tabs/tabs-firefox.js create mode 100644 addon-sdk/source/lib/sdk/tabs/utils.js create mode 100644 addon-sdk/source/lib/sdk/tabs/worker.js create mode 100644 addon-sdk/source/lib/sdk/test.js create mode 100644 addon-sdk/source/lib/sdk/test/assert.js create mode 100644 addon-sdk/source/lib/sdk/test/harness.js create mode 100644 addon-sdk/source/lib/sdk/test/httpd.js create mode 100644 addon-sdk/source/lib/sdk/test/loader.js create mode 100644 addon-sdk/source/lib/sdk/test/memory.js create mode 100644 addon-sdk/source/lib/sdk/test/options.js create mode 100644 addon-sdk/source/lib/sdk/test/runner.js create mode 100644 addon-sdk/source/lib/sdk/test/utils.js create mode 100644 addon-sdk/source/lib/sdk/timers.js create mode 100644 addon-sdk/source/lib/sdk/ui.js create mode 100644 addon-sdk/source/lib/sdk/ui/button/action.js create mode 100644 addon-sdk/source/lib/sdk/ui/button/contract.js create mode 100644 addon-sdk/source/lib/sdk/ui/button/toggle.js create mode 100644 addon-sdk/source/lib/sdk/ui/button/view.js create mode 100644 addon-sdk/source/lib/sdk/ui/button/view/events.js create mode 100644 addon-sdk/source/lib/sdk/ui/component.js create mode 100644 addon-sdk/source/lib/sdk/ui/frame.js create mode 100644 addon-sdk/source/lib/sdk/ui/frame/model.js create mode 100644 addon-sdk/source/lib/sdk/ui/frame/view.html create mode 100644 addon-sdk/source/lib/sdk/ui/frame/view.js create mode 100644 addon-sdk/source/lib/sdk/ui/id.js create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar.js create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/actions.js create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/contract.js create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/namespace.js create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/utils.js create mode 100644 addon-sdk/source/lib/sdk/ui/sidebar/view.js create mode 100644 addon-sdk/source/lib/sdk/ui/state.js create mode 100644 addon-sdk/source/lib/sdk/ui/state/events.js create mode 100644 addon-sdk/source/lib/sdk/ui/toolbar.js create mode 100644 addon-sdk/source/lib/sdk/ui/toolbar/model.js create mode 100644 addon-sdk/source/lib/sdk/ui/toolbar/view.js create mode 100644 addon-sdk/source/lib/sdk/uri/resource.js create mode 100644 addon-sdk/source/lib/sdk/url.js create mode 100644 addon-sdk/source/lib/sdk/url/utils.js create mode 100644 addon-sdk/source/lib/sdk/util/array.js create mode 100644 addon-sdk/source/lib/sdk/util/collection.js create mode 100644 addon-sdk/source/lib/sdk/util/contract.js create mode 100644 addon-sdk/source/lib/sdk/util/deprecate.js create mode 100644 addon-sdk/source/lib/sdk/util/dispatcher.js create mode 100644 addon-sdk/source/lib/sdk/util/list.js create mode 100644 addon-sdk/source/lib/sdk/util/match-pattern.js create mode 100644 addon-sdk/source/lib/sdk/util/object.js create mode 100644 addon-sdk/source/lib/sdk/util/rules.js create mode 100644 addon-sdk/source/lib/sdk/util/sequence.js create mode 100644 addon-sdk/source/lib/sdk/util/uuid.js create mode 100644 addon-sdk/source/lib/sdk/view/core.js create mode 100644 addon-sdk/source/lib/sdk/webextension.js create mode 100644 addon-sdk/source/lib/sdk/window/browser.js create mode 100644 addon-sdk/source/lib/sdk/window/events.js create mode 100644 addon-sdk/source/lib/sdk/window/helpers.js create mode 100644 addon-sdk/source/lib/sdk/window/namespace.js create mode 100644 addon-sdk/source/lib/sdk/window/utils.js create mode 100644 addon-sdk/source/lib/sdk/windows.js create mode 100644 addon-sdk/source/lib/sdk/windows/fennec.js create mode 100644 addon-sdk/source/lib/sdk/windows/firefox.js create mode 100644 addon-sdk/source/lib/sdk/windows/observer.js create mode 100644 addon-sdk/source/lib/sdk/windows/tabs-fennec.js create mode 100644 addon-sdk/source/lib/sdk/worker/utils.js create mode 100644 addon-sdk/source/lib/sdk/zip/utils.js create mode 100644 addon-sdk/source/lib/test.js create mode 100644 addon-sdk/source/lib/toolkit/loader.js create mode 100644 addon-sdk/source/lib/toolkit/require.js create mode 100644 addon-sdk/source/mapping.json create mode 100644 addon-sdk/source/modules/system/Startup.js create mode 100644 addon-sdk/source/modules/system/moz.build create mode 100644 addon-sdk/source/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/__init__.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/_version.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/bunch.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/manifest.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/mobile-utils/bootstrap.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/mobile-utils/install.rdf create mode 100644 addon-sdk/source/python-lib/cuddlefish/packaging.py create mode 100755 addon-sdk/source/python-lib/cuddlefish/preflight.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/prefs.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/property_parser.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/rdf.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/runner.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/templates.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/__init__.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/explicit-icon.png create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/explicit-icon64.png create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/icon.png create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/icon64.png create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/no-icon/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/no-icon/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/bar/lib/bar-loader.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/bar/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/foo/lib/foo-loader.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/foo/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/docs/main.md create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/doc/foo.md create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/lib/foo-loader.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/test/test-foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/docs/foo.md create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/lib/foo-loader.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/tests/test-foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/doc/foo.md create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/lib/foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/lib/loader.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/test/test-foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-locale/locale/emptyFolder create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-locale/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/doc/foo.md create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/loader.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/test/test-foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/alt-lib/foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/alt-lib/loader.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/doc/foo.md create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/test/test-foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/alt2-lib/foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/alt2-lib/loader.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/doc/foo.md create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/test/test-foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/docs/main.md create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-1-pack/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-2-unpack/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-3-pack/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/fullName/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/none/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/title/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/bar-e10s-adapter.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/bar.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/five/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/five/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/lib/misc.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/topfiles/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/subdir/three.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/two.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/data/text.data create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/lib/unused.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/lib/unused.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/unreachable.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/subdir/subfile.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/unused.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/locale/fr-FR.properties create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/locale/fr-FR.properties create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/lib/sub/foo.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/locale/fr-FR.properties create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/data/msg.txt create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/data/subdir/submsg.txt create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/nontest.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/test-one.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/test-two.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/doc/main.md create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/ignore_me create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/surprise.js/ignore_me_too create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/anteater_files/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/anteater_files/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/api-utils/lib/loader.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/api-utils/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/barbeque/lib/bar-module.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/barbeque/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/minimal/lib/main.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/minimal/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/docs/third_party.md create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/lib/third-party.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/package.json create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/static-files/xpi-template/components/harness.js create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_init.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_licenses.py create mode 100755 addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_manifest.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_packaging.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_preflight.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_property_parser.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_rdf.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_runner.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_util.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_version.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/util.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/version_comparator.py create mode 100644 addon-sdk/source/python-lib/cuddlefish/xpi.py create mode 100644 addon-sdk/source/python-lib/jetpack_sdk_env.py create mode 100644 addon-sdk/source/python-lib/mozrunner/__init__.py create mode 100644 addon-sdk/source/python-lib/mozrunner/killableprocess.py create mode 100644 addon-sdk/source/python-lib/mozrunner/qijo.py create mode 100644 addon-sdk/source/python-lib/mozrunner/winprocess.py create mode 100644 addon-sdk/source/python-lib/mozrunner/wpk.py create mode 100644 addon-sdk/source/python-lib/plural-rules-generator.py create mode 100644 addon-sdk/source/python-lib/simplejson/LICENSE.txt create mode 100644 addon-sdk/source/python-lib/simplejson/__init__.py create mode 100644 addon-sdk/source/python-lib/simplejson/decoder.py create mode 100644 addon-sdk/source/python-lib/simplejson/encoder.py create mode 100644 addon-sdk/source/python-lib/simplejson/scanner.py create mode 100644 addon-sdk/source/python-lib/simplejson/tool.py create mode 100644 addon-sdk/source/test/addons/addon-manager/lib/main.js create mode 100644 addon-sdk/source/test/addons/addon-manager/lib/test-main.js create mode 100644 addon-sdk/source/test/addons/addon-manager/package.json create mode 100644 addon-sdk/source/test/addons/author-email/main.js create mode 100644 addon-sdk/source/test/addons/author-email/package.json create mode 100644 addon-sdk/source/test/addons/child_process/index.js create mode 100644 addon-sdk/source/test/addons/child_process/package.json create mode 100644 addon-sdk/source/test/addons/chrome/chrome.manifest create mode 100644 addon-sdk/source/test/addons/chrome/chrome/content/new-window.xul create mode 100644 addon-sdk/source/test/addons/chrome/chrome/content/panel.html create mode 100644 addon-sdk/source/test/addons/chrome/chrome/locale/en-US/description.properties create mode 100644 addon-sdk/source/test/addons/chrome/chrome/locale/ja-JP/description.properties create mode 100644 addon-sdk/source/test/addons/chrome/chrome/skin/style.css create mode 100644 addon-sdk/source/test/addons/chrome/data/panel.js create mode 100644 addon-sdk/source/test/addons/chrome/main.js create mode 100644 addon-sdk/source/test/addons/chrome/package.json create mode 100644 addon-sdk/source/test/addons/content-permissions/httpd.js create mode 100644 addon-sdk/source/test/addons/content-permissions/main.js create mode 100644 addon-sdk/source/test/addons/content-permissions/package.json create mode 100644 addon-sdk/source/test/addons/content-script-messages-latency/httpd.js create mode 100644 addon-sdk/source/test/addons/content-script-messages-latency/main.js create mode 100644 addon-sdk/source/test/addons/content-script-messages-latency/package.json create mode 100644 addon-sdk/source/test/addons/contributors/main.js create mode 100644 addon-sdk/source/test/addons/contributors/package.json create mode 100644 addon-sdk/source/test/addons/curly-id/lib/main.js create mode 100644 addon-sdk/source/test/addons/curly-id/package.json create mode 100644 addon-sdk/source/test/addons/developers/main.js create mode 100644 addon-sdk/source/test/addons/developers/package.json create mode 100644 addon-sdk/source/test/addons/e10s-content/data/test-contentScriptFile.js create mode 100644 addon-sdk/source/test/addons/e10s-content/data/test-page-worker.html create mode 100644 addon-sdk/source/test/addons/e10s-content/data/test-page-worker.js create mode 100644 addon-sdk/source/test/addons/e10s-content/data/test.html create mode 100644 addon-sdk/source/test/addons/e10s-content/lib/fixtures.js create mode 100644 addon-sdk/source/test/addons/e10s-content/lib/httpd.js create mode 100644 addon-sdk/source/test/addons/e10s-content/lib/main.js create mode 100644 addon-sdk/source/test/addons/e10s-content/lib/test-content-script.js create mode 100644 addon-sdk/source/test/addons/e10s-content/lib/test-content-worker.js create mode 100644 addon-sdk/source/test/addons/e10s-content/lib/test-page-worker.js create mode 100644 addon-sdk/source/test/addons/e10s-content/package.json create mode 100644 addon-sdk/source/test/addons/e10s-l10n/data/test-localization.html create mode 100644 addon-sdk/source/test/addons/e10s-l10n/locale/en.properties create mode 100644 addon-sdk/source/test/addons/e10s-l10n/locale/eo.properties create mode 100644 addon-sdk/source/test/addons/e10s-l10n/locale/fr-FR.properties create mode 100644 addon-sdk/source/test/addons/e10s-l10n/main.js create mode 100644 addon-sdk/source/test/addons/e10s-l10n/package.json create mode 100644 addon-sdk/source/test/addons/e10s-remote/main.js create mode 100644 addon-sdk/source/test/addons/e10s-remote/package.json create mode 100644 addon-sdk/source/test/addons/e10s-remote/remote-module.js create mode 100644 addon-sdk/source/test/addons/e10s-remote/utils.js create mode 100644 addon-sdk/source/test/addons/e10s-tabs/lib/main.js create mode 100644 addon-sdk/source/test/addons/e10s-tabs/lib/private-browsing/helper.js create mode 100644 addon-sdk/source/test/addons/e10s-tabs/lib/test-tab-events.js create mode 100644 addon-sdk/source/test/addons/e10s-tabs/lib/test-tab-observer.js create mode 100644 addon-sdk/source/test/addons/e10s-tabs/lib/test-tab-utils.js create mode 100644 addon-sdk/source/test/addons/e10s-tabs/lib/test-tab.js create mode 100644 addon-sdk/source/test/addons/e10s-tabs/package.json create mode 100644 addon-sdk/source/test/addons/e10s/lib/main.js create mode 100644 addon-sdk/source/test/addons/e10s/package.json create mode 100644 addon-sdk/source/test/addons/embedded-webextension/main.js create mode 100644 addon-sdk/source/test/addons/embedded-webextension/package.json create mode 100644 addon-sdk/source/test/addons/embedded-webextension/webextension/background-page.js create mode 100644 addon-sdk/source/test/addons/embedded-webextension/webextension/content-script.js create mode 100644 addon-sdk/source/test/addons/embedded-webextension/webextension/manifest.json create mode 100644 addon-sdk/source/test/addons/jetpack-addon.ini create mode 100644 addon-sdk/source/test/addons/l10n-properties/app-extension/application.ini create mode 100644 addon-sdk/source/test/addons/l10n-properties/app-extension/bootstrap.js create mode 100644 addon-sdk/source/test/addons/l10n-properties/app-extension/install.rdf create mode 100644 addon-sdk/source/test/addons/l10n-properties/app-extension/locale/en-GB.properties create mode 100644 addon-sdk/source/test/addons/l10n-properties/app-extension/locale/en-US.properties create mode 100644 addon-sdk/source/test/addons/l10n-properties/app-extension/locale/eo.properties create mode 100644 addon-sdk/source/test/addons/l10n-properties/app-extension/locale/fr-FR.properties create mode 100644 addon-sdk/source/test/addons/l10n-properties/data/test-localization.html create mode 100644 addon-sdk/source/test/addons/l10n-properties/main.js create mode 100644 addon-sdk/source/test/addons/l10n-properties/package.json create mode 100644 addon-sdk/source/test/addons/l10n/data/test-localization.html create mode 100644 addon-sdk/source/test/addons/l10n/locale/en.properties create mode 100644 addon-sdk/source/test/addons/l10n/locale/eo.properties create mode 100644 addon-sdk/source/test/addons/l10n/locale/fr-FR.properties create mode 100644 addon-sdk/source/test/addons/l10n/main.js create mode 100644 addon-sdk/source/test/addons/l10n/package.json create mode 100644 addon-sdk/source/test/addons/layout-change/lib/main.js create mode 100644 addon-sdk/source/test/addons/layout-change/lib/test-cuddlefish-loader.js create mode 100644 addon-sdk/source/test/addons/layout-change/lib/test-toolkit-loader.js create mode 100644 addon-sdk/source/test/addons/layout-change/package.json create mode 100644 addon-sdk/source/test/addons/main/main.js create mode 100644 addon-sdk/source/test/addons/main/package.json create mode 100644 addon-sdk/source/test/addons/name-in-numbers-plus/index.js create mode 100644 addon-sdk/source/test/addons/name-in-numbers-plus/package.json create mode 100644 addon-sdk/source/test/addons/name-in-numbers/index.js create mode 100644 addon-sdk/source/test/addons/name-in-numbers/package.json create mode 100644 addon-sdk/source/test/addons/packaging/main.js create mode 100644 addon-sdk/source/test/addons/packaging/package.json create mode 100644 addon-sdk/source/test/addons/packed/main.js create mode 100644 addon-sdk/source/test/addons/packed/package.json create mode 100644 addon-sdk/source/test/addons/page-mod-debugger-post/data/index.html create mode 100644 addon-sdk/source/test/addons/page-mod-debugger-post/data/script.js create mode 100644 addon-sdk/source/test/addons/page-mod-debugger-post/main.js create mode 100644 addon-sdk/source/test/addons/page-mod-debugger-post/package.json create mode 100644 addon-sdk/source/test/addons/page-mod-debugger-pre/data/index.html create mode 100644 addon-sdk/source/test/addons/page-mod-debugger-pre/data/script.js create mode 100644 addon-sdk/source/test/addons/page-mod-debugger-pre/main.js create mode 100644 addon-sdk/source/test/addons/page-mod-debugger-pre/package.json create mode 100644 addon-sdk/source/test/addons/page-worker/data/page.html create mode 100644 addon-sdk/source/test/addons/page-worker/data/page.js create mode 100644 addon-sdk/source/test/addons/page-worker/main.js create mode 100644 addon-sdk/source/test/addons/page-worker/package.json create mode 100644 addon-sdk/source/test/addons/places/lib/favicon-helpers.js create mode 100644 addon-sdk/source/test/addons/places/lib/httpd.js create mode 100644 addon-sdk/source/test/addons/places/lib/main.js create mode 100644 addon-sdk/source/test/addons/places/lib/places-helper.js create mode 100644 addon-sdk/source/test/addons/places/lib/test-places-bookmarks.js create mode 100644 addon-sdk/source/test/addons/places/lib/test-places-events.js create mode 100644 addon-sdk/source/test/addons/places/lib/test-places-favicon.js create mode 100644 addon-sdk/source/test/addons/places/lib/test-places-history.js create mode 100644 addon-sdk/source/test/addons/places/lib/test-places-host.js create mode 100644 addon-sdk/source/test/addons/places/lib/test-places-utils.js create mode 100644 addon-sdk/source/test/addons/places/package.json create mode 100644 addon-sdk/source/test/addons/predefined-id-with-at/lib/main.js create mode 100644 addon-sdk/source/test/addons/predefined-id-with-at/package.json create mode 100644 addon-sdk/source/test/addons/preferences-branch/lib/main.js create mode 100644 addon-sdk/source/test/addons/preferences-branch/package.json create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/main.js create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/package.json create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/sidebar/utils.js create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/test-page-mod.js create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/test-panel.js create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/test-private-browsing.js create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/test-selection.js create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/test-sidebar.js create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/test-tabs.js create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/test-window-tabs.js create mode 100644 addon-sdk/source/test/addons/private-browsing-supported/test-windows.js create mode 100644 addon-sdk/source/test/addons/remote/main.js create mode 100644 addon-sdk/source/test/addons/remote/package.json create mode 100644 addon-sdk/source/test/addons/remote/remote-module.js create mode 100644 addon-sdk/source/test/addons/remote/utils.js create mode 100644 addon-sdk/source/test/addons/require/list.js create mode 100644 addon-sdk/source/test/addons/require/main.js create mode 100644 addon-sdk/source/test/addons/require/multiple/a.js create mode 100644 addon-sdk/source/test/addons/require/multiple/b.js create mode 100644 addon-sdk/source/test/addons/require/package.json create mode 100644 addon-sdk/source/test/addons/require/packages/tabs/main.js create mode 100644 addon-sdk/source/test/addons/require/packages/tabs/package.json create mode 100644 addon-sdk/source/test/addons/require/packages/tabs/page-mod.js create mode 100644 addon-sdk/source/test/addons/require/same-folder.js create mode 100644 addon-sdk/source/test/addons/require/sub-folder/module.js create mode 100644 addon-sdk/source/test/addons/require/tabs.js create mode 100644 addon-sdk/source/test/addons/self/data/data.md create mode 100644 addon-sdk/source/test/addons/self/main.js create mode 100644 addon-sdk/source/test/addons/self/package.json create mode 100644 addon-sdk/source/test/addons/simple-prefs-l10n/locale/en.properties create mode 100644 addon-sdk/source/test/addons/simple-prefs-l10n/main.js create mode 100644 addon-sdk/source/test/addons/simple-prefs-l10n/package.json create mode 100644 addon-sdk/source/test/addons/simple-prefs-regression/app-extension/application.ini create mode 100644 addon-sdk/source/test/addons/simple-prefs-regression/app-extension/bootstrap.js create mode 100644 addon-sdk/source/test/addons/simple-prefs-regression/app-extension/defaults/preferences/prefs.js create mode 100644 addon-sdk/source/test/addons/simple-prefs-regression/app-extension/install.rdf create mode 100644 addon-sdk/source/test/addons/simple-prefs-regression/app-extension/options.xul create mode 100644 addon-sdk/source/test/addons/simple-prefs-regression/lib/main.js create mode 100644 addon-sdk/source/test/addons/simple-prefs-regression/package.json create mode 100644 addon-sdk/source/test/addons/simple-prefs/lib/main.js create mode 100644 addon-sdk/source/test/addons/simple-prefs/package.json create mode 100644 addon-sdk/source/test/addons/standard-id/lib/main.js create mode 100644 addon-sdk/source/test/addons/standard-id/package.json create mode 100644 addon-sdk/source/test/addons/tab-close-on-startup/main.js create mode 100644 addon-sdk/source/test/addons/tab-close-on-startup/package.json create mode 100644 addon-sdk/source/test/addons/toolkit-require-reload/main.js create mode 100644 addon-sdk/source/test/addons/toolkit-require-reload/package.json create mode 100644 addon-sdk/source/test/addons/translators/main.js create mode 100644 addon-sdk/source/test/addons/translators/package.json create mode 100644 addon-sdk/source/test/addons/unsafe-content-script/main.js create mode 100644 addon-sdk/source/test/addons/unsafe-content-script/package.json create mode 100644 addon-sdk/source/test/buffers/test-read-types.js create mode 100644 addon-sdk/source/test/buffers/test-write-types.js create mode 100644 addon-sdk/source/test/commonjs-test-adapter/asserts.js create mode 100644 addon-sdk/source/test/context-menu/framescript.js create mode 100644 addon-sdk/source/test/context-menu/test-helper.js create mode 100644 addon-sdk/source/test/context-menu/util.js create mode 100644 addon-sdk/source/test/event/helpers.js create mode 100644 addon-sdk/source/test/fixtures.js create mode 100644 addon-sdk/source/test/fixtures/addon-install-unit-test@mozilla.com.xpi create mode 100644 addon-sdk/source/test/fixtures/addon-sdk/data/border-style.css create mode 100644 addon-sdk/source/test/fixtures/addon-sdk/data/test-contentScriptFile.js create mode 100644 addon-sdk/source/test/fixtures/addon-sdk/data/test-page-worker.html create mode 100644 addon-sdk/source/test/fixtures/addon-sdk/data/test-page-worker.js create mode 100644 addon-sdk/source/test/fixtures/addon-sdk/data/test.html create mode 100644 addon-sdk/source/test/fixtures/addon/bootstrap.js create mode 100644 addon-sdk/source/test/fixtures/addon/index.js create mode 100644 addon-sdk/source/test/fixtures/addon/package.json create mode 100644 addon-sdk/source/test/fixtures/bootstrap-addon/META-INF/manifest.mf create mode 100644 addon-sdk/source/test/fixtures/bootstrap-addon/META-INF/mozilla.rsa create mode 100644 addon-sdk/source/test/fixtures/bootstrap-addon/META-INF/mozilla.sf create mode 100644 addon-sdk/source/test/fixtures/bootstrap-addon/bootstrap.js create mode 100644 addon-sdk/source/test/fixtures/bootstrap-addon/install.rdf create mode 100644 addon-sdk/source/test/fixtures/bootstrap-addon/options.xul create mode 100644 addon-sdk/source/test/fixtures/bootstrap/utils.js create mode 100644 addon-sdk/source/test/fixtures/border-style.css create mode 100644 addon-sdk/source/test/fixtures/child-process-scripts.js create mode 100644 addon-sdk/source/test/fixtures/chrome-worker/addEventListener.js create mode 100644 addon-sdk/source/test/fixtures/chrome-worker/jsctypes.js create mode 100644 addon-sdk/source/test/fixtures/chrome-worker/onerror.js create mode 100644 addon-sdk/source/test/fixtures/chrome-worker/onmessage.js create mode 100644 addon-sdk/source/test/fixtures/chrome-worker/setTimeout.js create mode 100644 addon-sdk/source/test/fixtures/chrome-worker/xhr.js create mode 100644 addon-sdk/source/test/fixtures/create_xpi.py create mode 100644 addon-sdk/source/test/fixtures/es5.js create mode 100644 addon-sdk/source/test/fixtures/include-file.css create mode 100644 addon-sdk/source/test/fixtures/index.html create mode 100644 addon-sdk/source/test/fixtures/jsm-package/Test.jsm create mode 100644 addon-sdk/source/test/fixtures/jsm-package/index.js create mode 100644 addon-sdk/source/test/fixtures/jsm-package/package.json create mode 100644 addon-sdk/source/test/fixtures/loader/cycles/a.js create mode 100644 addon-sdk/source/test/fixtures/loader/cycles/b.js create mode 100644 addon-sdk/source/test/fixtures/loader/cycles/c.js create mode 100644 addon-sdk/source/test/fixtures/loader/cycles/main.js create mode 100644 addon-sdk/source/test/fixtures/loader/errors/boomer.js create mode 100644 addon-sdk/source/test/fixtures/loader/errors/main.js create mode 100644 addon-sdk/source/test/fixtures/loader/exceptions/boomer.js create mode 100644 addon-sdk/source/test/fixtures/loader/exceptions/main.js create mode 100644 addon-sdk/source/test/fixtures/loader/globals/main.js create mode 100644 addon-sdk/source/test/fixtures/loader/json/invalid.json create mode 100644 addon-sdk/source/test/fixtures/loader/json/manifest.json create mode 100644 addon-sdk/source/test/fixtures/loader/json/mutation.json create mode 100644 addon-sdk/source/test/fixtures/loader/json/nodotjson.json.js create mode 100644 addon-sdk/source/test/fixtures/loader/json/test.json create mode 100644 addon-sdk/source/test/fixtures/loader/json/test.json.js create mode 100644 addon-sdk/source/test/fixtures/loader/lazy/main.js create mode 100644 addon-sdk/source/test/fixtures/loader/missing-twice/file.json create mode 100644 addon-sdk/source/test/fixtures/loader/missing-twice/main.js create mode 100644 addon-sdk/source/test/fixtures/loader/missing/main.js create mode 100644 addon-sdk/source/test/fixtures/loader/self/main.js create mode 100644 addon-sdk/source/test/fixtures/loader/syntax-error/error.js create mode 100644 addon-sdk/source/test/fixtures/loader/syntax-error/main.js create mode 100644 addon-sdk/source/test/fixtures/loader/unsupported/fennec.js create mode 100644 addon-sdk/source/test/fixtures/loader/unsupported/firefox.js create mode 100644 addon-sdk/source/test/fixtures/mofo_logo.SVG create mode 100644 addon-sdk/source/test/fixtures/moz.build create mode 100644 addon-sdk/source/test/fixtures/moz_favicon.ico create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/dir/a.js create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/dir/a/index.js create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/dir/b.js create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/dir/c.js create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/dir/dummy.js create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/dir/test.jsm create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/expectedmap.json create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/index.js create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/newmodule/index.js create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/newmodule/lib/file.js create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/newmodule/package.json create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/package.json create mode 100644 addon-sdk/source/test/fixtures/native-addon-test/utils/index.js create mode 100644 addon-sdk/source/test/fixtures/native-overrides-test/ignore.js create mode 100644 addon-sdk/source/test/fixtures/native-overrides-test/index.js create mode 100644 addon-sdk/source/test/fixtures/native-overrides-test/lib/ignore.js create mode 100644 addon-sdk/source/test/fixtures/native-overrides-test/lib/internal.js create mode 100644 addon-sdk/source/test/fixtures/native-overrides-test/lib/tabs.js create mode 100644 addon-sdk/source/test/fixtures/native-overrides-test/package.json create mode 100644 addon-sdk/source/test/fixtures/preferences/curly-id/package.json create mode 100644 addon-sdk/source/test/fixtures/preferences/no-prefs/package.json create mode 100644 addon-sdk/source/test/fixtures/preferences/preferences-branch/package.json create mode 100644 addon-sdk/source/test/fixtures/preferences/simple-prefs/package.json create mode 100644 addon-sdk/source/test/fixtures/sandbox-complex-character.js create mode 100644 addon-sdk/source/test/fixtures/sandbox-normal.js create mode 100644 addon-sdk/source/test/fixtures/test-addon-extras-window.html create mode 100644 addon-sdk/source/test/fixtures/test-addon-extras.html create mode 100644 addon-sdk/source/test/fixtures/test-contentScriptFile.js create mode 100644 addon-sdk/source/test/fixtures/test-context-menu.js create mode 100644 addon-sdk/source/test/fixtures/test-iframe-postmessage.html create mode 100644 addon-sdk/source/test/fixtures/test-iframe.html create mode 100644 addon-sdk/source/test/fixtures/test-iframe.js create mode 100644 addon-sdk/source/test/fixtures/test-message-manager.js create mode 100644 addon-sdk/source/test/fixtures/test-net-url.txt create mode 100644 addon-sdk/source/test/fixtures/test-page-mod.html create mode 100644 addon-sdk/source/test/fixtures/test-sidebar-addon-global.html create mode 100644 addon-sdk/source/test/fixtures/test-trusted-document.html create mode 100644 addon-sdk/source/test/fixtures/test.html create mode 100644 addon-sdk/source/test/fixtures/testLocalXhr.json create mode 100644 addon-sdk/source/test/framescript-manager/frame-script.js create mode 100644 addon-sdk/source/test/framescript-manager/pong.js create mode 100644 addon-sdk/source/test/framescript-util/frame-script.js create mode 100644 addon-sdk/source/test/jetpack-package.ini create mode 100644 addon-sdk/source/test/leak/jetpack-package.ini create mode 100644 addon-sdk/source/test/leak/leak-utils.js create mode 100644 addon-sdk/source/test/leak/test-leak-event-dom-closed-window.js create mode 100644 addon-sdk/source/test/leak/test-leak-tab-events.js create mode 100644 addon-sdk/source/test/leak/test-leak-window-events.js create mode 100644 addon-sdk/source/test/lib/httpd.js create mode 100644 addon-sdk/source/test/loader/b2g.js create mode 100644 addon-sdk/source/test/loader/fixture.js create mode 100644 addon-sdk/source/test/loader/user-global.js create mode 100644 addon-sdk/source/test/modules/add.js create mode 100644 addon-sdk/source/test/modules/async1.js create mode 100644 addon-sdk/source/test/modules/async2.js create mode 100644 addon-sdk/source/test/modules/badExportAndReturn.js create mode 100644 addon-sdk/source/test/modules/badFirst.js create mode 100644 addon-sdk/source/test/modules/badSecond.js create mode 100644 addon-sdk/source/test/modules/blue.js create mode 100644 addon-sdk/source/test/modules/castor.js create mode 100644 addon-sdk/source/test/modules/cheetah.js create mode 100644 addon-sdk/source/test/modules/color.js create mode 100644 addon-sdk/source/test/modules/dupe.js create mode 100644 addon-sdk/source/test/modules/dupeNested.js create mode 100644 addon-sdk/source/test/modules/dupeSetExports.js create mode 100644 addon-sdk/source/test/modules/exportsEquals.js create mode 100644 addon-sdk/source/test/modules/green.js create mode 100644 addon-sdk/source/test/modules/lion.js create mode 100644 addon-sdk/source/test/modules/orange.js create mode 100644 addon-sdk/source/test/modules/pollux.js create mode 100644 addon-sdk/source/test/modules/red.js create mode 100644 addon-sdk/source/test/modules/setExports.js create mode 100644 addon-sdk/source/test/modules/subtract.js create mode 100644 addon-sdk/source/test/modules/tiger.js create mode 100644 addon-sdk/source/test/modules/traditional1.js create mode 100644 addon-sdk/source/test/modules/traditional2.js create mode 100644 addon-sdk/source/test/modules/types/cat.js create mode 100644 addon-sdk/source/test/page-mod/helpers.js create mode 100644 addon-sdk/source/test/path/test-path.js create mode 100644 addon-sdk/source/test/preferences/common.json create mode 100644 addon-sdk/source/test/preferences/e10s-off.json create mode 100644 addon-sdk/source/test/preferences/e10s-on.json create mode 100644 addon-sdk/source/test/preferences/firefox.json create mode 100644 addon-sdk/source/test/preferences/no-connections.json create mode 100644 addon-sdk/source/test/preferences/test-e10s-preferences.js create mode 100644 addon-sdk/source/test/preferences/test-preferences.js create mode 100644 addon-sdk/source/test/preferences/test.json create mode 100644 addon-sdk/source/test/private-browsing/helper.js create mode 100644 addon-sdk/source/test/private-browsing/tabs.js create mode 100644 addon-sdk/source/test/private-browsing/windows.js create mode 100644 addon-sdk/source/test/querystring/test-querystring.js create mode 100644 addon-sdk/source/test/sidebar/utils.js create mode 100644 addon-sdk/source/test/tabs/test-fennec-tabs.js create mode 100644 addon-sdk/source/test/tabs/test-firefox-tabs.js create mode 100644 addon-sdk/source/test/tabs/utils.js create mode 100644 addon-sdk/source/test/test-addon-bootstrap.js create mode 100644 addon-sdk/source/test/test-addon-extras.js create mode 100644 addon-sdk/source/test/test-addon-installer.js create mode 100644 addon-sdk/source/test/test-addon-window.js create mode 100644 addon-sdk/source/test/test-api-utils.js create mode 100644 addon-sdk/source/test/test-array.js create mode 100644 addon-sdk/source/test/test-base64.js create mode 100644 addon-sdk/source/test/test-bootstrap.js create mode 100644 addon-sdk/source/test/test-browser-events.js create mode 100644 addon-sdk/source/test/test-buffer.js create mode 100644 addon-sdk/source/test/test-byte-streams.js create mode 100644 addon-sdk/source/test/test-child_process.js create mode 100644 addon-sdk/source/test/test-chrome.js create mode 100644 addon-sdk/source/test/test-clipboard.js create mode 100644 addon-sdk/source/test/test-collection.js create mode 100644 addon-sdk/source/test/test-commonjs-test-adapter.js create mode 100644 addon-sdk/source/test/test-content-events.js create mode 100644 addon-sdk/source/test/test-content-script.js create mode 100644 addon-sdk/source/test/test-content-sync-worker.js create mode 100644 addon-sdk/source/test/test-content-worker.js create mode 100644 addon-sdk/source/test/test-context-menu.html create mode 100644 addon-sdk/source/test/test-context-menu.js create mode 100644 addon-sdk/source/test/test-context-menu@2.js create mode 100644 addon-sdk/source/test/test-cuddlefish.js create mode 100644 addon-sdk/source/test/test-deprecate.js create mode 100644 addon-sdk/source/test/test-dev-panel.js create mode 100644 addon-sdk/source/test/test-diffpatcher.js create mode 100644 addon-sdk/source/test/test-dispatcher.js create mode 100644 addon-sdk/source/test/test-disposable.js create mode 100644 addon-sdk/source/test/test-dom.js create mode 100644 addon-sdk/source/test/test-environment.js create mode 100644 addon-sdk/source/test/test-event-core.js create mode 100644 addon-sdk/source/test/test-event-dom.js create mode 100644 addon-sdk/source/test/test-event-target.js create mode 100644 addon-sdk/source/test/test-event-utils.js create mode 100644 addon-sdk/source/test/test-file.js create mode 100644 addon-sdk/source/test/test-frame-utils.js create mode 100644 addon-sdk/source/test/test-framescript-manager.js create mode 100644 addon-sdk/source/test/test-framescript-util.js create mode 100644 addon-sdk/source/test/test-fs.js create mode 100644 addon-sdk/source/test/test-functional.js create mode 100644 addon-sdk/source/test/test-globals.js create mode 100644 addon-sdk/source/test/test-heritage.js create mode 100644 addon-sdk/source/test/test-hidden-frame.js create mode 100644 addon-sdk/source/test/test-host-events.js create mode 100644 addon-sdk/source/test/test-hotkeys.js create mode 100644 addon-sdk/source/test/test-httpd.js create mode 100644 addon-sdk/source/test/test-indexed-db.js create mode 100644 addon-sdk/source/test/test-jetpack-id.js create mode 100644 addon-sdk/source/test/test-keyboard-observer.js create mode 100644 addon-sdk/source/test/test-keyboard-utils.js create mode 100644 addon-sdk/source/test/test-l10n-locale.js create mode 100644 addon-sdk/source/test/test-l10n-plural-rules.js create mode 100644 addon-sdk/source/test/test-lang-type.js create mode 100644 addon-sdk/source/test/test-libxul.js create mode 100644 addon-sdk/source/test/test-list.js create mode 100644 addon-sdk/source/test/test-loader.js create mode 100644 addon-sdk/source/test/test-match-pattern.js create mode 100644 addon-sdk/source/test/test-method.js create mode 100644 addon-sdk/source/test/test-module.js create mode 100644 addon-sdk/source/test/test-modules.js create mode 100644 addon-sdk/source/test/test-mozilla-toolkit-versioning.js create mode 100644 addon-sdk/source/test/test-mpl2-license-header.js create mode 100644 addon-sdk/source/test/test-namespace.js create mode 100644 addon-sdk/source/test/test-native-loader.js create mode 100644 addon-sdk/source/test/test-native-options.js create mode 100644 addon-sdk/source/test/test-net-url.js create mode 100644 addon-sdk/source/test/test-node-os.js create mode 100644 addon-sdk/source/test/test-notifications.js create mode 100644 addon-sdk/source/test/test-object.js create mode 100644 addon-sdk/source/test/test-observers.js create mode 100644 addon-sdk/source/test/test-page-mod-debug.js create mode 100644 addon-sdk/source/test/test-page-mod.js create mode 100644 addon-sdk/source/test/test-page-worker.js create mode 100644 addon-sdk/source/test/test-panel.js create mode 100644 addon-sdk/source/test/test-passwords-utils.js create mode 100644 addon-sdk/source/test/test-passwords.js create mode 100644 addon-sdk/source/test/test-path.js create mode 100644 addon-sdk/source/test/test-plain-text-console.js create mode 100644 addon-sdk/source/test/test-preferences-service.js create mode 100644 addon-sdk/source/test/test-preferences-target.js create mode 100644 addon-sdk/source/test/test-private-browsing.js create mode 100644 addon-sdk/source/test/test-promise.js create mode 100644 addon-sdk/source/test/test-querystring.js create mode 100644 addon-sdk/source/test/test-reference.js create mode 100644 addon-sdk/source/test/test-request.js create mode 100644 addon-sdk/source/test/test-require.js create mode 100644 addon-sdk/source/test/test-rules.js create mode 100644 addon-sdk/source/test/test-sandbox.js create mode 100644 addon-sdk/source/test/test-selection.js create mode 100644 addon-sdk/source/test/test-self.js create mode 100644 addon-sdk/source/test/test-sequence.js create mode 100644 addon-sdk/source/test/test-set-exports.js create mode 100644 addon-sdk/source/test/test-shared-require.js create mode 100644 addon-sdk/source/test/test-simple-prefs.js create mode 100644 addon-sdk/source/test/test-simple-storage.js create mode 100644 addon-sdk/source/test/test-system-events.js create mode 100644 addon-sdk/source/test/test-system-input-output.js create mode 100644 addon-sdk/source/test/test-system-runtime.js create mode 100644 addon-sdk/source/test/test-system-startup.js create mode 100644 addon-sdk/source/test/test-system.js create mode 100644 addon-sdk/source/test/test-tab-events.js create mode 100644 addon-sdk/source/test/test-tab-observer.js create mode 100644 addon-sdk/source/test/test-tab-utils.js create mode 100644 addon-sdk/source/test/test-tab.js create mode 100644 addon-sdk/source/test/test-tabs-common.js create mode 100644 addon-sdk/source/test/test-tabs.js create mode 100644 addon-sdk/source/test/test-test-addon-file.js create mode 100644 addon-sdk/source/test/test-test-assert.js create mode 100644 addon-sdk/source/test/test-test-loader.js create mode 100644 addon-sdk/source/test/test-test-memory.js create mode 100644 addon-sdk/source/test/test-test-utils-async.js create mode 100644 addon-sdk/source/test/test-test-utils-generator.js create mode 100644 addon-sdk/source/test/test-test-utils-sync.js create mode 100644 addon-sdk/source/test/test-test-utils.js create mode 100644 addon-sdk/source/test/test-text-streams.js create mode 100644 addon-sdk/source/test/test-timer.js create mode 100644 addon-sdk/source/test/test-traceback.js create mode 100644 addon-sdk/source/test/test-ui-action-button.js create mode 100644 addon-sdk/source/test/test-ui-frame.js create mode 100644 addon-sdk/source/test/test-ui-id.js create mode 100644 addon-sdk/source/test/test-ui-sidebar-private-browsing.js create mode 100644 addon-sdk/source/test/test-ui-sidebar.js create mode 100644 addon-sdk/source/test/test-ui-toggle-button.js create mode 100644 addon-sdk/source/test/test-ui-toolbar.js create mode 100644 addon-sdk/source/test/test-unit-test-finder.js create mode 100644 addon-sdk/source/test/test-unit-test.js create mode 100644 addon-sdk/source/test/test-unload.js create mode 100644 addon-sdk/source/test/test-unsupported-skip.js create mode 100644 addon-sdk/source/test/test-uri-resource.js create mode 100644 addon-sdk/source/test/test-url.js create mode 100644 addon-sdk/source/test/test-uuid.js create mode 100644 addon-sdk/source/test/test-weak-set.js create mode 100644 addon-sdk/source/test/test-window-events.js create mode 100644 addon-sdk/source/test/test-window-observer.js create mode 100644 addon-sdk/source/test/test-window-utils-private-browsing.js create mode 100644 addon-sdk/source/test/test-window-utils.js create mode 100644 addon-sdk/source/test/test-window-utils2.js create mode 100644 addon-sdk/source/test/test-windows-common.js create mode 100644 addon-sdk/source/test/test-windows.js create mode 100644 addon-sdk/source/test/test-xhr.js create mode 100644 addon-sdk/source/test/test-xpcom.js create mode 100644 addon-sdk/source/test/test-xul-app.js create mode 100644 addon-sdk/source/test/traits/assert.js create mode 100644 addon-sdk/source/test/traits/utils.js create mode 100644 addon-sdk/source/test/util.js create mode 100644 addon-sdk/source/test/windows/test-fennec-windows.js create mode 100644 addon-sdk/source/test/windows/test-firefox-windows.js create mode 100644 addon-sdk/source/test/zip/utils.js (limited to 'addon-sdk/source') diff --git a/addon-sdk/source/.gitattributes b/addon-sdk/source/.gitattributes new file mode 100644 index 000000000..9006725a3 --- /dev/null +++ b/addon-sdk/source/.gitattributes @@ -0,0 +1,5 @@ +.gitignore export-ignore +.hgignore export-ignore +.hgtags export-ignore +.gitattributes export-ignore +python-lib/cuddlefish/_version.py export-subst diff --git a/addon-sdk/source/.gitignore b/addon-sdk/source/.gitignore new file mode 100644 index 000000000..80d235bd1 --- /dev/null +++ b/addon-sdk/source/.gitignore @@ -0,0 +1,36 @@ +local.json +python-lib/cuddlefish/app-extension/components/jetpack.xpt +testdocs.tgz +jetpack-sdk-docs.tgz +.test_tmp/ +doc/dev-guide/ +doc/index.html +doc/modules/ +doc/status.md5 +packages/* +node_modules +cache + +# Python +*.pyc + +# OSX +*.DS_Store + +# Windows +*Thumbs.db + +# Ignore subtrees + +# git@github.com:jsantell/jetpack-id.git +lib/jetpack-id/* +!lib/jetpack-id/index.js +!lib/jetpack-id/package.json + +# git@github.com:jsantell/mozilla-toolkit-versioning.git +lib/mozilla-toolkit-versioning/* +!lib/mozilla-toolkit-versioning/index.js +!lib/mozilla-toolkit-versioning/lib +lib/mozilla-toolkit-versioning/lib/* +!lib/mozilla-toolkit-versioning/lib/*.js +!lib/mozilla-toolkit-versioning/package.json diff --git a/addon-sdk/source/.jpmignore b/addon-sdk/source/.jpmignore new file mode 100644 index 000000000..5c22f9c45 --- /dev/null +++ b/addon-sdk/source/.jpmignore @@ -0,0 +1,18 @@ +local.json +mapping.json +CONTRIBUTING.md +@addon-sdk.xpi +.* +app-extension/ +bin/ +modules/ +node_modules/ +examples/ +cache/ + +# 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 000000000..287b62a4f --- /dev/null +++ b/addon-sdk/source/.travis.yml @@ -0,0 +1,26 @@ +sudo: false +language: node_js +node_js: + - "0.12" + +env: + - JPM_FX_DEBUG=0 + - JPM_FX_DEBUG=1 + +notifications: + irc: "irc.mozilla.org#jetpack" + +cache: + directories: + - cache + +before_install: + - "export DISPLAY=:99.0" + - "/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 fx-download -g + - npm install gulp -g + - bash bin/fx-download.sh + - export JPM_FIREFOX_BINARY=$TRAVIS_BUILD_DIR/../firefox/firefox + - cd $TRAVIS_BUILD_DIR diff --git a/addon-sdk/source/CONTRIBUTING.md b/addon-sdk/source/CONTRIBUTING.md new file mode 100644 index 000000000..059df64c4 --- /dev/null +++ b/addon-sdk/source/CONTRIBUTING.md @@ -0,0 +1,54 @@ +## Overview + +- Changes should follow the [design guidelines], as well as the [coding style guide] +- All changes need tests +- In order to land, changes need a review by one of the Jetpack reviewers +- Changes may need an API review +- Changes may need a review from a Mozilla platform domain-expert + +If you have questions, ask in [#jetpack on IRC][jetpack irc channel] or on the [Jetpack mailing list]. + +## How to Make Code Contributions + +Follow the [standard mozilla contribution guidelines](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Introduction). All contributions and patches should be through Bugzilla. + +Pull requests on github are not accepted and new pull requests on github will be rejected. + +## Good First Bugs + +There is a list of [good first bugs here][good first bugs]. + +## Reviewers + +Changes should be reviewed by someone on the [add-on sdk review team](https://bugzilla.mozilla.org/page.cgi?id=review_suggestions.html#Add-on%20SDK) within Bugzilla. + +Others who might be able to help include: + +- [@mossop] +- [@gozala] +- [@ZER0] +- [@jsantell] +- [@zombie] + +For review of Mozilla platform usage and best practices, ask [@autonome], +[@0c0w3], or [@mossop] to find the domain expert. + +For API and developer ergonomics review, ask [@gozala]. + +[design guidelines]:https://wiki.mozilla.org/Labs/Jetpack/Design_Guidelines +[jetpack irc channel]:irc://irc.mozilla.org/#jetpack +[Jetpack mailing list]:http://groups.google.com/group/mozilla-labs-jetpack +[open bugs]:https://bugzilla.mozilla.org/buglist.cgi?quicksearch=product%3ASDK +[make bug]:https://bugzilla.mozilla.org/enter_bug.cgi?product=Add-on%20SDK&component=general +[test intro]:https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Unit_testing +[test API]:https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/test_assert +[coding style guide]:https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide +[Add-on SDK repo]:https://github.com/mozilla/addon-sdk +[GitHub]:https://github.com/ +[good first bugs]:https://bugzilla.mozilla.org/buglist.cgi?list_id=7345714&columnlist=bug_severity%2Cpriority%2Cassigned_to%2Cbug_status%2Ctarget_milestone%2Cresolution%2Cshort_desc%2Cchangeddate&query_based_on=jetpack-good-1st-bugs&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=[good%20first%20bug]&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bug_status=VERIFIED&product=Add-on%20SDK&known_name=jetpack-good-1st-bugs + +[@mossop]:https://github.com/mossop/ +[@gozala]:https://github.com/Gozala/ +[@ZER0]:https://github.com/ZER0/ +[@jsantell]:https://github.com/jsantell +[@zombie]:https://github.com/zombie diff --git a/addon-sdk/source/LICENSE b/addon-sdk/source/LICENSE new file mode 100644 index 000000000..22e1dc915 --- /dev/null +++ b/addon-sdk/source/LICENSE @@ -0,0 +1,30 @@ +The files which make up the SDK are developed by Mozilla and licensed +under the MPL 2.0 (http://mozilla.org/MPL/2.0/), with the exception of the +components listed below, which are made available by their authors under +the licenses listed alongside. + +syntaxhighlighter +------------------ +doc/static-files/syntaxhighlighter +Made available under the MIT license. + +jQuery +------ +examples/reddit-panel/data/jquery-1.4.4.min.js +examples/annotator/data/jquery-1.4.2.min.js +Made available under the MIT license. + +simplejson +---------- +python-lib/simplejson +Made available under the MIT license. + +Python Markdown +--------------- +python-lib/markdown +Made available under the BSD license. + +LibraryDetector +--------------- +examples/library-detector/data/library-detector.js +Made available under the MIT license. diff --git a/addon-sdk/source/README.md b/addon-sdk/source/README.md new file mode 100644 index 000000000..4efa86dae --- /dev/null +++ b/addon-sdk/source/README.md @@ -0,0 +1,34 @@ +# Mozilla Add-on SDK [![Build Status](https://travis-ci.org/mozilla/addon-sdk.png)](https://travis-ci.org/mozilla/addon-sdk) + +We suggest that developers of new add-ons [should look at using WebExtensions](https://developer.mozilla.org/en-US/Add-ons/WebExtensions). + +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 offer 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 make some patches to the addon-sdk: + +* [Contribute Guide](https://github.com/mozilla/addon-sdk/blob/master/CONTRIBUTING.md) +* [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) +* [Good next bugs](https://bugzilla.mozilla.org/buglist.cgi?status_whiteboard=[good+next+bug]&&resolution=---&product=Add-on+SDK) diff --git a/addon-sdk/source/app-extension/application.ini b/addon-sdk/source/app-extension/application.ini new file mode 100644 index 000000000..6cec69a16 --- /dev/null +++ b/addon-sdk/source/app-extension/application.ini @@ -0,0 +1,11 @@ +[App] +Vendor=Varma +Name=Test App +Version=1.0 +BuildID=20060101 +Copyright=Copyright (c) 2009 Atul Varma +ID=xulapp@toolness.com + +[Gecko] +MinVersion=1.9.2.0 +MaxVersion=2.0.* diff --git a/addon-sdk/source/app-extension/bootstrap.js b/addon-sdk/source/app-extension/bootstrap.js new file mode 100644 index 000000000..c2207c75f --- /dev/null +++ b/addon-sdk/source/app-extension/bootstrap.js @@ -0,0 +1,362 @@ +/* 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/. */ + +// @see http://dxr.mozilla.org/mozilla-central/source/js/src/xpconnect/loader/mozJSComponentLoader.cpp + +'use strict'; + +// IMPORTANT: Avoid adding any initialization tasks here, if you need to do +// something before add-on is loaded consider addon/runner module instead! + +const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu, + results: Cr, manager: Cm } = Components; +const ioService = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); +const resourceHandler = ioService.getProtocolHandler('resource'). + QueryInterface(Ci.nsIResProtocolHandler); +const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); +const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1']. + getService(Ci.mozIJSSubScriptLoader); +const prefService = Cc['@mozilla.org/preferences-service;1']. + getService(Ci.nsIPrefService). + QueryInterface(Ci.nsIPrefBranch); +const appInfo = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULAppInfo); +const vc = Cc["@mozilla.org/xpcom/version-comparator;1"]. + getService(Ci.nsIVersionComparator); + +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); + +const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports; + + +const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable', + 'install', 'uninstall', 'upgrade', 'downgrade' ]; + +const bind = Function.call.bind(Function.bind); + +var loader = null; +var unload = null; +var cuddlefishSandbox = null; +var nukeTimer = null; + +var resourceDomains = []; +function setResourceSubstitution(domain, uri) { + resourceDomains.push(domain); + resourceHandler.setSubstitution(domain, uri); +} + +// Utility function that synchronously reads local resource from the given +// `uri` and returns content string. +function readURI(uri) { + let channel = NetUtil.newChannel({ + uri: NetUtil.newURI(uri, 'UTF-8'), + loadUsingSystemPrincipal: true + }); + + let stream = channel.open2(); + + let cstream = Cc['@mozilla.org/intl/converter-input-stream;1']. + createInstance(Ci.nsIConverterInputStream); + cstream.init(stream, 'UTF-8', 0, 0); + + let str = {}; + let data = ''; + let read = 0; + do { + read = cstream.readString(0xffffffff, str); + data += str.value; + } while (read != 0); + + cstream.close(); + + return data; +} + +// We don't do anything on install & uninstall yet, but in a future +// we should allow add-ons to cleanup after uninstall. +function install(data, reason) {} +function uninstall(data, reason) {} + +function startup(data, reasonCode) { + try { + let reason = REASON[reasonCode]; + // URI for the root of the XPI file. + // 'jar:' URI if the addon is packed, 'file:' URI otherwise. + // (Used by l10n module in order to fetch `locale` folder) + let rootURI = data.resourceURI.spec; + + // TODO: Maybe we should perform read harness-options.json asynchronously, + // since we can't do anything until 'sessionstore-windows-restored' anyway. + let options = JSON.parse(readURI(rootURI + './harness-options.json')); + + let id = options.jetpackID; + let name = options.name; + + // Clean the metadata + options.metadata[name]['permissions'] = options.metadata[name]['permissions'] || {}; + + // freeze the permissionss + Object.freeze(options.metadata[name]['permissions']); + // freeze the metadata + Object.freeze(options.metadata[name]); + + // Register a new resource 'domain' for this addon which is mapping to + // XPI's `resources` folder. + // Generate the domain name by using jetpack ID, which is the extension ID + // by stripping common characters that doesn't work as a domain name: + let uuidRe = + /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/; + + let domain = id. + toLowerCase(). + replace(/@/g, '-at-'). + replace(/\./g, '-dot-'). + replace(uuidRe, '$1'); + + let prefixURI = 'resource://' + domain + '/'; + let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null); + setResourceSubstitution(domain, resourcesURI); + + // Create path to URLs mapping supported by loader. + let paths = { + // Relative modules resolve to add-on package lib + './': prefixURI + name + '/lib/', + './tests/': prefixURI + name + '/tests/', + '': 'resource://gre/modules/commonjs/' + }; + + // Maps addon lib and tests ressource folders for each package + paths = Object.keys(options.metadata).reduce(function(result, name) { + result[name + '/'] = prefixURI + name + '/lib/' + result[name + '/tests/'] = prefixURI + name + '/tests/' + return result; + }, paths); + + // We need to map tests folder when we run sdk tests whose package name + // is stripped + if (name == 'addon-sdk') + paths['tests/'] = prefixURI + name + '/tests/'; + + let useBundledSDK = options['force-use-bundled-sdk']; + if (!useBundledSDK) { + try { + useBundledSDK = prefService.getBoolPref("extensions.addon-sdk.useBundledSDK"); + } + catch (e) { + // Pref doesn't exist, allow using Firefox shipped SDK + } + } + + // Starting with Firefox 21.0a1, we start using modules shipped into firefox + // Still allow using modules from the xpi if the manifest tell us to do so. + // And only try to look for sdk modules in xpi if the xpi actually ship them + if (options['is-sdk-bundled'] && + (vc.compare(appInfo.version, '21.0a1') < 0 || useBundledSDK)) { + // Maps sdk module folders to their resource folder + paths[''] = prefixURI + 'addon-sdk/lib/'; + // test.js is usually found in root commonjs or SDK_ROOT/lib/ folder, + // so that it isn't shipped in the xpi. Keep a copy of it in sdk/ folder + // until we no longer support SDK modules in XPI: + paths['test'] = prefixURI + 'addon-sdk/lib/sdk/test.js'; + } + + // Retrieve list of module folder overloads based on preferences in order to + // eventually used a local modules instead of files shipped into Firefox. + let branch = prefService.getBranch('extensions.modules.' + id + '.path'); + paths = branch.getChildList('', {}).reduce(function (result, name) { + // Allows overloading of any sub folder by replacing . by / in pref name + let path = name.substr(1).split('.').join('/'); + // Only accept overloading folder by ensuring always ending with `/` + if (path) path += '/'; + let fileURI = branch.getCharPref(name); + + // On mobile, file URI has to end with a `/` otherwise, setSubstitution + // takes the parent folder instead. + if (fileURI[fileURI.length-1] !== '/') + fileURI += '/'; + + // Maps the given file:// URI to a resource:// in order to avoid various + // failure that happens with file:// URI and be close to production env + let resourcesURI = ioService.newURI(fileURI, null, null); + let resName = 'extensions.modules.' + domain + '.commonjs.path' + name; + setResourceSubstitution(resName, resourcesURI); + + result[path] = 'resource://' + resName + '/'; + return result; + }, paths); + + // Make version 2 of the manifest + let manifest = options.manifest; + + // Import `cuddlefish.js` module using a Sandbox and bootstrap loader. + let cuddlefishPath = 'loader/cuddlefish.js'; + let cuddlefishURI = 'resource://gre/modules/commonjs/sdk/' + cuddlefishPath; + if (paths['sdk/']) { // sdk folder has been overloaded + // (from pref, or cuddlefish is still in the xpi) + cuddlefishURI = paths['sdk/'] + cuddlefishPath; + } + else if (paths['']) { // root modules folder has been overloaded + cuddlefishURI = paths[''] + 'sdk/' + cuddlefishPath; + } + + cuddlefishSandbox = loadSandbox(cuddlefishURI); + let cuddlefish = cuddlefishSandbox.exports; + + // Normalize `options.mainPath` so that it looks like one that will come + // in a new version of linker. + let main = options.mainPath; + + unload = cuddlefish.unload; + loader = cuddlefish.Loader({ + paths: paths, + // modules manifest. + manifest: manifest, + + // Add-on ID used by different APIs as a unique identifier. + id: id, + // Add-on name. + name: name, + // Add-on version. + version: options.metadata[name].version, + // Add-on package descriptor. + metadata: options.metadata[name], + // Add-on load reason. + loadReason: reason, + + prefixURI: prefixURI, + // Add-on URI. + rootURI: rootURI, + // options used by system module. + // File to write 'OK' or 'FAIL' (exit code emulation). + resultFile: options.resultFile, + // Arguments passed as --static-args + staticArgs: options.staticArgs, + + // Option to prevent automatic kill of firefox during tests + noQuit: options.no_quit, + + // Add-on preferences branch name + preferencesBranch: options.preferencesBranch, + + // Arguments related to test runner. + modules: { + '@test/options': { + iterations: options.iterations, + filter: options.filter, + profileMemory: options.profileMemory, + stopOnError: options.stopOnError, + verbose: options.verbose, + parseable: options.parseable, + checkMemory: options.check_memory, + } + } + }); + + let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI); + let require = cuddlefish.Require(loader, module); + + // Init the 'sdk/webextension' module from the bootstrap addon parameter. + require("sdk/webextension").initFromBootstrapAddonParam(data); + + require('sdk/addon/runner').startup(reason, { + loader: loader, + main: main, + prefsURI: rootURI + 'defaults/preferences/prefs.js' + }); + } catch (error) { + dump('Bootstrap error: ' + + (error.message ? error.message : String(error)) + '\n' + + (error.stack || error.fileName + ': ' + error.lineNumber) + '\n'); + throw error; + } +}; + +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: bind(CC, Components), components: Components, + ChromeWorker: ChromeWorker }); + }; + scriptLoader.loadSubScript(uri, sandbox, 'UTF-8'); + return sandbox; +} + +function unloadSandbox(sandbox) { + if (Cu.getClassName(sandbox, true) == "Sandbox") + Cu.nukeSandbox(sandbox); +} + +function setTimeout(callback, delay) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback({ notify: callback }, delay, + Ci.nsITimer.TYPE_ONE_SHOT); + return timer; +} + +function shutdown(data, reasonCode) { + let reason = REASON[reasonCode]; + if (loader) { + unload(loader, reason); + unload = null; + + // Don't waste time cleaning up if the application is shutting down + if (reason != "shutdown") { + // Avoid leaking all modules when something goes wrong with one particular + // module. Do not clean it up immediatly in order to allow executing some + // actions on addon disabling. + // We need to keep a reference to the timer, otherwise it is collected + // and won't ever fire. + nukeTimer = setTimeout(nukeModules, 1000); + + // Bug 944951 - bootstrap.js must remove the added resource: URIs on unload + resourceDomains.forEach(domain => { + resourceHandler.setSubstitution(domain, null); + }) + } + } +}; + +function nukeModules() { + nukeTimer = null; + // module objects store `exports` which comes from sandboxes + // We should avoid keeping link to these object to avoid leaking sandboxes + for (let key in loader.modules) { + delete loader.modules[key]; + } + // Direct links to sandboxes should be removed too + for (let key in loader.sandboxes) { + let sandbox = loader.sandboxes[key]; + delete loader.sandboxes[key]; + // Bug 775067: From FF17 we can kill all CCW from a given sandbox + unloadSandbox(sandbox); + } + unloadSandbox(loader.sharedGlobalSandbox); + loader = null; + + // both `toolkit/loader` and `system/xul-app` are loaded as JSM's via + // `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when + // the addon is unload. + + unloadSandbox(cuddlefishSandbox.loaderSandbox); + + // Bug 764840: We need to unload cuddlefish otherwise it will stay alive + // and keep a reference to this compartment. + unloadSandbox(cuddlefishSandbox); + cuddlefishSandbox = null; +} diff --git a/addon-sdk/source/app-extension/install.rdf b/addon-sdk/source/app-extension/install.rdf new file mode 100644 index 000000000..641d1cc21 --- /dev/null +++ b/addon-sdk/source/app-extension/install.rdf @@ -0,0 +1,33 @@ + + + + + + xulapp@toolness.com + 1.0 + 2 + true + false + + + + + {ec8030f7-c20a-464f-9b0e-13a3a9e97384} + 26.0 + 30.0 + + + + + Test App + Harness for tests. + Mozilla Corporation + + + + + + diff --git a/addon-sdk/source/bin/activate b/addon-sdk/source/bin/activate new file mode 100644 index 000000000..0104f7d9d --- /dev/null +++ b/addon-sdk/source/bin/activate @@ -0,0 +1,84 @@ +# 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/. + +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + if [ -n "$_OLD_VIRTUAL_PATH" ] ; then + PATH="$_OLD_VIRTUAL_PATH" + export PATH + unset _OLD_VIRTUAL_PATH + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "$BASH" -o -n "$ZSH_VERSION" ] ; then + hash -r + fi + + if [ -n "$_OLD_VIRTUAL_PS1" ] ; then + PS1="$_OLD_VIRTUAL_PS1" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + PYTHONPATH="$_OLD_PYTHONPATH" + export PYTHONPATH + unset _OLD_PYTHONPATH + + unset CUDDLEFISH_ROOT + + unset VIRTUAL_ENV + if [ ! "$1" = "nondestructive" ] ; then + # Self destruct! + unset deactivate + fi +} + +# unset irrelavent variables +deactivate nondestructive + +_OLD_PYTHONPATH="$PYTHONPATH" +_OLD_VIRTUAL_PATH="$PATH" + +VIRTUAL_ENV="`pwd`" + +if [ "x$OSTYPE" = "xmsys" ] ; then + CUDDLEFISH_ROOT="`pwd -W | sed s,/,\\\\\\\\,g`" + PATH="`pwd`/bin:$PATH" + # msys will convert any env vars with PATH in it to use msys + # form and will unconvert before launching + PYTHONPATH="`pwd -W`/python-lib;$PYTHONPATH" +else + CUDDLEFISH_ROOT="$VIRTUAL_ENV" + PYTHONPATH="$VIRTUAL_ENV/python-lib:$PYTHONPATH" + PATH="$VIRTUAL_ENV/bin:$PATH" +fi + +VIRTUAL_ENV="`pwd`" + +export CUDDLEFISH_ROOT +export PYTHONPATH +export PATH + +_OLD_VIRTUAL_PS1="$PS1" +if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then + # special case for Aspen magic directories + # see http://www.zetadev.com/software/aspen/ + PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1" +else + PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1" +fi +export PS1 + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "$BASH" -o -n "$ZSH_VERSION" ] ; then + hash -r +fi + +python -c "from jetpack_sdk_env import welcome; welcome()" diff --git a/addon-sdk/source/bin/activate.bat b/addon-sdk/source/bin/activate.bat new file mode 100644 index 000000000..7d1f968a7 --- /dev/null +++ b/addon-sdk/source/bin/activate.bat @@ -0,0 +1,134 @@ +@echo off +rem This Source Code Form is subject to the terms of the Mozilla Public +rem License, v. 2.0. If a copy of the MPL was not distributed with this +rem file, You can obtain one at http://mozilla.org/MPL/2.0/. + +set VIRTUAL_ENV=%~dp0 +set VIRTUAL_ENV=%VIRTUAL_ENV:~0,-5% +set CUDDLEFISH_ROOT=%VIRTUAL_ENV% + +SET PYTHONKEY=SOFTWARE\Python\PythonCore + +rem look for 32-bit windows and python, or 64-bit windows and python + +SET PYTHONVERSION=2.7 +call:CheckPython PYTHONINSTALL %PYTHONKEY%\%PYTHONVERSION%\InstallPath +if "%PYTHONINSTALL%" NEQ "" goto FoundPython + +SET PYTHONVERSION=2.6 +call:CheckPython PYTHONINSTALL %PYTHONKEY%\%PYTHONVERSION%\InstallPath +if "%PYTHONINSTALL%" NEQ "" goto FoundPython + +SET PYTHONVERSION=2.5 +call:CheckPython PYTHONINSTALL %PYTHONKEY%\%PYTHONVERSION%\InstallPath +if "%PYTHONINSTALL%" NEQ "" goto FoundPython + +if not defined ProgramFiles(x86) goto win32 + +rem look for 32-bit python on 64-bit windows + +SET PYTHONKEY=SOFTWARE\Wow6432Node\Python\PythonCore + +SET PYTHONVERSION=2.7 +call:CheckPython PYTHONINSTALL %PYTHONKEY%\%PYTHONVERSION%\InstallPath +if "%PYTHONINSTALL%" NEQ "" goto FoundPython + +SET PYTHONVERSION=2.6 +call:CheckPython PYTHONINSTALL %PYTHONKEY%\%PYTHONVERSION%\InstallPath +if "%PYTHONINSTALL%" NEQ "" goto FoundPython + +SET PYTHONVERSION=2.5 +call:CheckPython PYTHONINSTALL %PYTHONKEY%\%PYTHONVERSION%\InstallPath +if "%PYTHONINSTALL%" NEQ "" goto FoundPython + +:win32 + +SET PYTHONVERSION= +set PYTHONKEY= +echo Warning: Failed to find Python installation directory +goto :EOF + +:FoundPython + +if defined _OLD_PYTHONPATH ( + set PYTHONPATH=%_OLD_PYTHONPATH% +) +if not defined PYTHONPATH ( + set PYTHONPATH=; +) +set _OLD_PYTHONPATH=%PYTHONPATH% +set PYTHONPATH=%VIRTUAL_ENV%\python-lib;%PYTHONPATH% + +if not defined PROMPT ( + set PROMPT=$P$G +) + +if defined _OLD_VIRTUAL_PROMPT ( + set PROMPT=%_OLD_VIRTUAL_PROMPT% +) + +set _OLD_VIRTUAL_PROMPT=%PROMPT% +set PROMPT=(%VIRTUAL_ENV%) %PROMPT% + +if defined _OLD_VIRTUAL_PATH goto OLDPATH +goto SKIPPATH +:OLDPATH +PATH %_OLD_VIRTUAL_PATH% + +:SKIPPATH +set _OLD_VIRTUAL_PATH=%PATH% +PATH %VIRTUAL_ENV%\bin;%PYTHONINSTALL%;%PATH% +set PYTHONKEY= +set PYTHONINSTALL= +set PYTHONVERSION= +set key= +set reg= +set _tokens= +python -c "from jetpack_sdk_env import welcome; welcome()" +GOTO :EOF + +:CheckPython +::CheckPython(retVal, key) +::Reads the registry at %2% and checks if a Python exists there. +::Checks both HKLM and HKCU, then checks the executable actually exists. +SET key=%2% +SET "%~1=" +SET reg=reg +if defined ProgramFiles(x86) ( + rem 32-bit cmd on 64-bit windows + if exist %WINDIR%\sysnative\reg.exe SET reg=%WINDIR%\sysnative\reg.exe +) +rem On Vista+, the last line of output is: +rem (default) REG_SZ the_value +rem (but note the word "default" will be localized. +rem On XP, the last line of output is: +rem \tREG_SZ\tthe_value +rem (not sure if "NO NAME" is localized or not!) +rem SO: we use ")>" as the tokens to split on, then nuke +rem the REG_SZ and any tabs or spaces. +FOR /F "usebackq tokens=2 delims=)>" %%A IN (`%reg% QUERY HKLM\%key% /ve 2^>NUL`) DO SET "%~1=%%A" +rem Remove the REG_SZ +set PYTHONINSTALL=%PYTHONINSTALL:REG_SZ=% +rem Remove tabs (note the literal \t in the next line +set PYTHONINSTALL=%PYTHONINSTALL: =% +rem Remove spaces. +set PYTHONINSTALL=%PYTHONINSTALL: =% +if exist %PYTHONINSTALL%\python.exe goto :EOF +rem It may be a 32bit Python directory built from source, in which case the +rem executable is in the PCBuild directory. +if exist %PYTHONINSTALL%\PCBuild\python.exe (set "PYTHONINSTALL=%PYTHONINSTALL%\PCBuild" & goto :EOF) +rem Or maybe a 64bit build directory. +if exist %PYTHONINSTALL%\PCBuild\amd64\python.exe (set "PYTHONINSTALL=%PYTHONINSTALL%\PCBuild\amd64" & goto :EOF) + +rem And try HKCU +FOR /F "usebackq tokens=2 delims=)>" %%A IN (`%reg% QUERY HKCU\%key% /ve 2^>NUL`) DO SET "%~1=%%A" +set PYTHONINSTALL=%PYTHONINSTALL:REG_SZ=% +set PYTHONINSTALL=%PYTHONINSTALL: =% +set PYTHONINSTALL=%PYTHONINSTALL: =% +if exist %PYTHONINSTALL%\python.exe goto :EOF +if exist %PYTHONINSTALL%\PCBuild\python.exe (set "PYTHONINSTALL=%PYTHONINSTALL%\PCBuild" & goto :EOF) +if exist %PYTHONINSTALL%\PCBuild\amd64\python.exe (set "PYTHONINSTALL=%PYTHONINSTALL%\PCBuild\amd64" & goto :EOF) +rem can't find it here, so arrange to try the next key +set PYTHONINSTALL= + +GOTO :EOF diff --git a/addon-sdk/source/bin/activate.fish b/addon-sdk/source/bin/activate.fish new file mode 100644 index 000000000..1f728b69b --- /dev/null +++ b/addon-sdk/source/bin/activate.fish @@ -0,0 +1,66 @@ +# 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/. + +# This file must be used with "source bin/activate.fish" *from fish* +# you cannot run it directly + +# Much of this code is based off of the activate.fish file for the +# virtualenv project. http://ur1.ca/ehmd6 + +function deactivate -d "Exit addon-sdk and return to normal shell environment" + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + + if test -n "$_OLD_PYTHONPATH" + set -gx PYTHONPATH $_OLD_PYTHONPATH + set -e _OLD_PYTHONPATH + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + functions -e fish_prompt + set -e _OLD_FISH_PROMPT_OVERRIDE + . ( begin + printf "function fish_prompt\n\t#" + functions _old_fish_prompt + end | psub ) + + functions -e _old_fish_prompt + end + + set -e CUDDLEFISH_ROOT + set -e VIRTUAL_ENV + + if test "$argv[1]" != "nondestructive" + functions -e deactivate + end +end + +# unset irrelavent variables +deactivate nondestructive + +set -gx _OLD_PYTHONPATH $PYTHONPATH +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx _OLD_FISH_PROMPT_OVERRIDE "true" + +set VIRTUAL_ENV (pwd) + +set -gx CUDDLEFISH_ROOT $VIRTUAL_ENV +set -gx PYTHONPATH "$VIRTUAL_ENV/python-lib" $PYTHONPATH +set -gx PATH "$VIRTUAL_ENV/bin" $PATH + +# save the current fish_prompt function as the function _old_fish_prompt +. ( begin + printf "function _old_fish_prompt\n\t#" + functions fish_prompt + end | psub ) + +# with the original prompt function renamed, we can override with our own. +function fish_prompt + printf "(%s)%s%s" (basename "$VIRTUAL_ENV") (set_color normal) (_old_fish_prompt) + return +end + +python -c "from jetpack_sdk_env import welcome; welcome()" diff --git a/addon-sdk/source/bin/activate.ps1 b/addon-sdk/source/bin/activate.ps1 new file mode 100644 index 000000000..5d939d468 --- /dev/null +++ b/addon-sdk/source/bin/activate.ps1 @@ -0,0 +1,99 @@ +# 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/. + +$Env:VIRTUAL_ENV = (gl); +$Env:CUDDLEFISH_ROOT = $Env:VIRTUAL_ENV; + +# http://stackoverflow.com/questions/5648931/powershell-test-if-registry-value-exists/5652674#5652674 +Function Test-RegistryValue { + param( + [Alias("PSPath")] + [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [String]$Path + , + [Parameter(Position = 1, Mandatory = $true)] + [String]$Name + , + [Switch]$PassThru + ) + + process { + if (Test-Path $Path) { + $Key = Get-Item -LiteralPath $Path + if ($Key.GetValue($Name, $null) -ne $null) { + if ($PassThru) { + Get-ItemProperty $Path $Name + } else { + $true + } + } else { + $false + } + } else { + $false + } + } +} + +$WINCURVERKEY = 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion'; +$WIN64 = (Test-RegistryValue $WINCURVERKEY 'ProgramFilesDir (x86)'); + +if($WIN64) { + $PYTHONKEY='HKLM:SOFTWARE\Wow6432Node\Python\PythonCore'; +} +else { + $PYTHONKEY='HKLM:SOFTWARE\Python\PythonCore'; +} + +$Env:PYTHONVERSION = ''; +$Env:PYTHONINSTALL = ''; + +foreach ($version in @('2.6', '2.5', '2.4')) { + if (Test-RegistryValue "$PYTHONKEY\$version\InstallPath" '(default)') { + $Env:PYTHONVERSION = $version; + } +} + +if ($Env:PYTHONVERSION) { + $Env:PYTHONINSTALL = (Get-Item "$PYTHONKEY\$version\InstallPath)").'(default)'; +} + +if ($Env:PYTHONINSTALL) { + $Env:Path += ";$Env:PYTHONINSTALL"; +} + +if (Test-Path Env:_OLD_PYTHONPATH) { + $Env:PYTHONPATH = $Env:_OLD_PYTHONPATH; +} +else { + $Env:PYTHONPATH = ''; +} + +$Env:_OLD_PYTHONPATH=$Env:PYTHONPATH; +$Env:PYTHONPATH= "$Env:VIRTUAL_ENV\python-lib;$Env:PYTHONPATH"; + +if (Test-Path Function:_OLD_VIRTUAL_PROMPT) { + Set-Content Function:Prompt (Get-Content Function:_OLD_VIRTUAL_PROMPT); +} +else { + function global:_OLD_VIRTUAL_PROMPT {} +} + +Set-Content Function:_OLD_VIRTUAL_PROMPT (Get-Content Function:Prompt); + +function global:prompt { "($Env:VIRTUAL_ENV) $(_OLD_VIRTUAL_PROMPT)"; }; + +if (Test-Path Env:_OLD_VIRTUAL_PATH) { + $Env:PATH = $Env:_OLD_VIRTUAL_PATH; +} +else { + $Env:_OLD_VIRTUAL_PATH = $Env:PATH; +} + +$Env:Path="$Env:VIRTUAL_ENV\bin;$Env:Path" + +[System.Console]::WriteLine("Note: this PowerShell SDK activation script is experimental.") + +python -c "from jetpack_sdk_env import welcome; welcome()" + diff --git a/addon-sdk/source/bin/cfx b/addon-sdk/source/bin/cfx new file mode 100755 index 000000000..3e781faf8 --- /dev/null +++ b/addon-sdk/source/bin/cfx @@ -0,0 +1,33 @@ +#! /usr/bin/env python +# 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/. + + +import os +import sys + +# set the cuddlefish "root directory" for this process if it's not already +# set in the environment +cuddlefish_root = os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0]))) + +if 'CUDDLEFISH_ROOT' not in os.environ: + os.environ['CUDDLEFISH_ROOT'] = cuddlefish_root + +# add our own python-lib path to the python module search path. +python_lib_dir = os.path.join(cuddlefish_root, "python-lib") +if python_lib_dir not in sys.path: + sys.path.insert(0, python_lib_dir) + +# now export to env so sub-processes get it too +if 'PYTHONPATH' not in os.environ: + os.environ['PYTHONPATH'] = python_lib_dir +elif python_lib_dir not in os.environ['PYTHONPATH'].split(os.pathsep): + paths = os.environ['PYTHONPATH'].split(os.pathsep) + paths.insert(0, python_lib_dir) + os.environ['PYTHONPATH'] = os.pathsep.join(paths) + +import cuddlefish + +if __name__ == '__main__': + cuddlefish.run() diff --git a/addon-sdk/source/bin/cfx.bat b/addon-sdk/source/bin/cfx.bat new file mode 100644 index 000000000..215b034f5 --- /dev/null +++ b/addon-sdk/source/bin/cfx.bat @@ -0,0 +1,6 @@ +@echo off +rem This Source Code Form is subject to the terms of the Mozilla Public +rem License, v. 2.0. If a copy of the MPL was not distributed with this +rem file, You can obtain one at http://mozilla.org/MPL/2.0/. + +python "%VIRTUAL_ENV%\bin\cfx" %* diff --git a/addon-sdk/source/bin/deactivate.bat b/addon-sdk/source/bin/deactivate.bat new file mode 100644 index 000000000..e6bcd9293 --- /dev/null +++ b/addon-sdk/source/bin/deactivate.bat @@ -0,0 +1,23 @@ +@echo off +rem This Source Code Form is subject to the terms of the Mozilla Public +rem License, v. 2.0. If a copy of the MPL was not distributed with this +rem file, You can obtain one at http://mozilla.org/MPL/2.0/. + +if defined _OLD_VIRTUAL_PROMPT ( + set "PROMPT=%_OLD_VIRTUAL_PROMPT%" +) +set _OLD_VIRTUAL_PROMPT= + +if defined _OLD_VIRTUAL_PATH ( + set "PATH=%_OLD_VIRTUAL_PATH%" +) +set _OLD_VIRTUAL_PATH= + +if defined _OLD_PYTHONPATH ( + set "PYTHONPATH=%_OLD_PYTHONPATH%" +) +set _OLD_PYTHONPATH= + +set CUDDLEFISH_ROOT= + +:END diff --git a/addon-sdk/source/bin/fx-download.sh b/addon-sdk/source/bin/fx-download.sh new file mode 100644 index 000000000..690208a38 --- /dev/null +++ b/addon-sdk/source/bin/fx-download.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +if [ "$JPM_FX_DEBUG" = "1" ]; then + fx-download --branch nightly -c prerelease --host ftp.mozilla.org ../firefox --debug +else + fx-download --branch nightly -c prerelease --host ftp.mozilla.org ../firefox +fi diff --git a/addon-sdk/source/bin/integration-scripts/buildbot-run-cfx-helper b/addon-sdk/source/bin/integration-scripts/buildbot-run-cfx-helper new file mode 100755 index 000000000..56c76ad32 --- /dev/null +++ b/addon-sdk/source/bin/integration-scripts/buildbot-run-cfx-helper @@ -0,0 +1,14 @@ +#!/bin/bash +# 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/. + + +source ./bin/activate +if [ type -P xvfb-run ] +then + xvfb-run cfx $* +else + cfx $* +fi +deactivate diff --git a/addon-sdk/source/bin/integration-scripts/integration-check b/addon-sdk/source/bin/integration-scripts/integration-check new file mode 100644 index 000000000..2505c15dc --- /dev/null +++ b/addon-sdk/source/bin/integration-scripts/integration-check @@ -0,0 +1,364 @@ +#!/usr/bin/env python +# 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/. + +import os +import signal +import threading +import urllib2, urllib +import zipfile +import tarfile +import subprocess +import optparse +import sys, re +#import win32api + + +class SDK: + def __init__(self): + try: + # Take the current working directory + self.default_path = os.getcwd() + if sys.platform == "win32": + self.mswindows = True + else: + self.mswindows = False + # Take the default home path of the user. + home = os.path.expanduser('~') + + # The following are the parameters that can be used to pass a dynamic URL, a specific path or a binry. The binary is not used yet. It will be used in version 2.0 + # If a dynamic path is to be mentioned, it should start with a '/'. For eg. "/Desktop" + parser = optparse.OptionParser() + parser.add_option('-u', '--url', dest = 'url', default = 'https://ftp.mozilla.org/pub/mozilla.org/labs/jetpack/addon-sdk-latest.zip') + parser.add_option('-p', '--path', dest = 'path', default = self.default_path) + parser.add_option('-b', '--binary', dest = 'binary')#, default='/Applications/Firefox.app') + (options, args) = parser.parse_args() + + # Get the URL from the parameter + self.link = options.url + # Set the base path for the user. If the user supplies the path, use the home variable as well. Else, take the default path of this script as the installation directory. + if options.path!=self.default_path: + if self.mswindows: + self.base_path = home + str(options.path).strip() + '\\' + else: + self.base_path = home + str(options.path).strip() + '/' + else: + if self.mswindows: + self.base_path = str(options.path).strip() + '\\' + else: + self.base_path = str(options.path).strip() + '/' + assert ' ' not in self.base_path, "You cannot have a space in your home path. Please remove the space before you continue." + print('Your Base path is =' + self.base_path) + + # This assignment is not used in this program. It will be used in version 2 of this script. + self.bin = options.binary + # if app or bin is empty, dont pass anything + + # Search for the .zip file or tarball file in the URL. + i = self.link.rfind('/') + + self.fname = self.link[i+1:] + z = re.search('zip',self.fname,re.I) + g = re.search('gz',self.fname,re.I) + if z: + print 'zip file present in the URL.' + self.zip = True + self.gz = False + elif g: + print 'gz file present in the URL' + self.gz = True + self.zip = False + else: + print 'zip/gz file not present. Check the URL.' + return + print("File name is =" + self.fname) + + # Join the base path and the zip/tar file name to crate a complete Local file path. + self.fpath = self.base_path + self.fname + print('Your local file path will be=' + self.fpath) + except AssertionError, e: + print e.args[0] + sys.exit(1) + + # Download function - to download the SDK from the URL to the local machine. + def download(self,url,fpath,fname): + try: + # Start the download + print("Downloading...Please be patient!") + urllib.urlretrieve(url,filename = fname) + print('Download was successful.') + except ValueError: # Handles broken URL errors. + print 'The URL is ether broken or the file does not exist. Please enter the correct URL.' + raise + except urllib2.URLError: # Handles URL errors + print '\nURL not correct. Check again!' + raise + + # Function to extract the downloaded zipfile. + def extract(self, zipfilepath, extfile): + try: + # Timeout is set to 30 seconds. + timeout = 30 + # Change the directory to the location of the zip file. + try: + os.chdir(zipfilepath) + except OSError: + # Will reach here if zip file doesnt exist + print 'O/S Error:' + zipfilepath + 'does not exist' + raise + + # Get the folder name of Jetpack to get the exact version number. + if self.zip: + try: + f = zipfile.ZipFile(extfile, "r") + except IOError as (errno, strerror): # Handles file errors + print "I/O error - Cannot perform extract operation: {1}".format(errno, strerror) + raise + list = f.namelist()[0] + temp_name = list.split('/') + print('Folder Name= ' +temp_name[0]) + self.folder_name = temp_name[0] + elif self.gz: + try: + f = tarfile.open(extfile,'r') + except IOError as (errno, strerror): # Handles file errors + print "I/O error - Cannot perform extract operation: {1}".format(errno, strerror) + raise + list = f.getnames()[0] + temp_name = list.split('/') + print('Folder Name= ' +temp_name[0]) + self.folder_name = temp_name[0] + + print ('Starting to Extract...') + + # Timeout code. The subprocess.popen exeutes the command and the thread waits for a timeout. If the process does not finish within the mentioned- + # timeout, the process is killed. + kill_check = threading.Event() + + if self.zip: + # Call the command to unzip the file. + if self.mswindows: + zipfile.ZipFile.extractall(f) + else: + p = subprocess.Popen('unzip '+extfile, stdout=subprocess.PIPE, shell=True) + pid = p.pid + elif self.gz: + # Call the command to untar the file. + if self.mswindows: + tarfile.TarFile.extractall(f) + else: + p = subprocess.Popen('tar -xf '+extfile, stdout=subprocess.PIPE, shell=True) + pid = p.pid + + #No need to handle for windows because windows automatically replaces old files with new files. It does not ask the user(as it does in Mac/Unix) + if self.mswindows==False: + watch = threading.Timer(timeout, kill_process, args=(pid, kill_check, self.mswindows )) + watch.start() + (stdout, stderr) = p.communicate() + watch.cancel() # if it's still waiting to run + success = not kill_check.isSet() + + # Abort process if process fails. + if not success: + raise RuntimeError + kill_check.clear() + print('Extraction Successful.') + except RuntimeError: + print "Ending the program" + sys.exit(1) + except: + print "Error during file extraction: ", sys.exc_info()[0] + raise + + # Function to run the cfx testall comands and to make sure the SDK is not broken. + def run_testall(self, home_path, folder_name): + try: + timeout = 500 + + self.new_dir = home_path + folder_name + try: + os.chdir(self.new_dir) + except OSError: + # Will reach here if the jetpack 0.X directory doesnt exist + print 'O/S Error: Jetpack directory does not exist at ' + self.new_dir + raise + print '\nStarting tests...' + # Timeout code. The subprocess.popen exeutes the command and the thread waits for a timeout. If the process does not finish within the mentioned- + # timeout, the process is killed. + kill_check = threading.Event() + + # Set the path for the logs. They will be in the parent directory of the Jetpack SDK. + log_path = home_path + 'tests.log' + + # Subprocess call to set up the jetpack environment and to start the tests. Also sends the output to a log file. + if self.bin != None: + if self.mswindows: + p = subprocess.Popen("bin\\activate && cfx testall -a firefox -b \"" + self.bin + "\"" , stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + proc_handle = p._handle + (stdout,stderr) = p.communicate() + else: + p = subprocess.Popen('. bin/activate; cfx testall -a firefox -b ' + self.bin , stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + pid = p.pid + (stdout,stderr) = p.communicate() + elif self.bin == None: + if self.mswindows: + p=subprocess.Popen('bin\\activate && cfx testall -a firefox > '+log_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + proc_handle = p._handle + (stdout,stderr) = p.communicate() + else: + p = subprocess.Popen('. bin/activate; cfx testall -a firefox > '+log_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + pid = p.pid + (stdout,stderr) = p.communicate() + + #Write the output to log file + f=open(log_path,"w") + f.write(stdout+stderr) + f.close() + + #Watchdog for timeout process + if self.mswindows: + watch = threading.Timer(timeout, kill_process, args=(proc_handle, kill_check, self.mswindows)) + else: + watch = threading.Timer(timeout, kill_process, args=(pid, kill_check, self.mswindows)) + watch.start() + watch.cancel() # if it's still waiting to run + success = not kill_check.isSet() + if not success: + raise RuntimeError + kill_check.clear() + + if p.returncode!=0: + print('\nAll tests were not successful. Check the test-logs in the jetpack directory.') + result_sdk(home_path) + #sys.exit(1) + raise RuntimeError + else: + ret_code=result_sdk(home_path) + if ret_code==0: + print('\nAll tests were successful. Yay \o/ . Running a sample package test now...') + else: + print ('\nThere were errors during the tests.Take a look at logs') + raise RuntimeError + except RuntimeError: + print "Ending the program" + sys.exit(1) + except: + print "Error during the testall command execution:", sys.exc_info()[0] + raise + + def package(self, example_dir): + try: + timeout = 30 + + print '\nNow Running packaging tests...' + + kill_check = threading.Event() + + # Set the path for the example logs. They will be in the parent directory of the Jetpack SDK. + exlog_path = example_dir + 'test-example.log' + # Subprocess call to test the sample example for packaging. + if self.bin!=None: + if self.mswindows: + p = subprocess.Popen('bin\\activate && cfx run --pkgdir examples\\reading-data --static-args="{\\"quitWhenDone\\":true}" -b \"" + self.bin + "\"' , stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + proc_handle = p._handle + (stdout, stderr) = p.communicate() + else: + p = subprocess.Popen('. bin/activate; cfx run --pkgdir examples/reading-data --static-args=\'{\"quitWhenDone\":true}\' -b ' + self.bin , stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + pid = p.pid + (stdout, stderr) = p.communicate() + elif self.bin==None: + if self.mswindows: + p = subprocess.Popen('bin\\activate && cfx run --pkgdir examples\\reading-data --static-args="{\\"quitWhenDone\\":true}"', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + proc_handle = p._handle + (stdout, stderr) = p.communicate() + else: + p = subprocess.Popen('. bin/activate; cfx run --pkgdir examples/reading-data --static-args=\'{\"quitWhenDone\":true}\' ', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + pid = p.pid + (stdout, stderr) = p.communicate() + + #Write the output to log file + f=open(exlog_path,"w") + f.write(stdout+stderr) + f.close() + + #Watch dog for timeout process + if self.mswindows: + watch = threading.Timer(timeout, kill_process, args=(proc_handle, kill_check, self.mswindows)) + else: + watch = threading.Timer(timeout, kill_process, args=(pid, kill_check, self.mswindows)) + watch.start() + watch.cancel() # if it's still waiting to run + success = not kill_check.isSet() + if not success: + raise RuntimeError + kill_check.clear() + + if p.returncode != 0: + print('\nSample tests were not executed correctly. Check the test-example log in jetpack diretory.') + result_example(example_dir) + raise RuntimeError + else: + ret_code=result_example(example_dir) + if ret_code==0: + print('\nAll tests pass. The SDK is working! Yay \o/') + else: + print ('\nTests passed with warning.Take a look at logs') + sys.exit(1) + + except RuntimeError: + print "Ending program" + sys.exit(1) + except: + print "Error during running sample tests:", sys.exc_info()[0] + raise + +def result_sdk(sdk_dir): + log_path = sdk_dir + 'tests.log' + print 'Results are logged at:' + log_path + try: + f = open(log_path,'r') + # Handles file errors + except IOError : + print 'I/O error - Cannot open test log at ' + log_path + raise + + for line in reversed(open(log_path).readlines()): + if line.strip()=='FAIL': + print ('\nOverall result - FAIL. Look at the test log at '+log_path) + return 1 + return 0 + + +def result_example(sdk_dir): + exlog_path = sdk_dir + 'test-example.log' + print 'Sample test results are logged at:' + exlog_path + try: + f = open(exlog_path,'r') + # Handles file errors + except IOError : + print 'I/O error - Cannot open sample test log at ' + exlog_path + raise + + #Read the file in reverse and check for the keyword 'FAIL'. + for line in reversed(open(exlog_path).readlines()): + if line.strip()=='FAIL': + print ('\nOverall result for Sample tests - FAIL. Look at the test log at '+exlog_path) + return 1 + return 0 + +def kill_process(process, kill_check, mswindows): + print '\nProcess Timedout. Killing the process. Please Rerun this script.' + if mswindows: + win32api.TerminateProcess(process, -1) + else: + os.kill(process, signal.SIGKILL) + kill_check.set()# tell the main routine to kill. Used SIGKILL to hard kill the process. + return + +if __name__ == "__main__": + obj = SDK() + obj.download(obj.link,obj.fpath,obj.fname) + obj.extract(obj.base_path,obj.fname) + obj.run_testall(obj.base_path,obj.folder_name) + obj.package(obj.base_path) diff --git a/addon-sdk/source/bin/jpm-test.js b/addon-sdk/source/bin/jpm-test.js new file mode 100644 index 000000000..f22a552ea --- /dev/null +++ b/addon-sdk/source/bin/jpm-test.js @@ -0,0 +1,34 @@ +/* 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 Promise = require("promise"); +var Mocha = require("mocha"); +var mocha = new Mocha({ + ui: "bdd", + reporter: "spec", + timeout: 900000 +}); + +var isDebug = require("./node-scripts/utils").isDebug; + +exports.run = function(type) { + return new Promise(function(resolve) { + type = type || ""; + [ + (!isDebug && /^(firefox-bin)?$/.test(type)) && require.resolve("../bin/node-scripts/test.firefox-bin"), + (!isDebug && /^(docs)?$/.test(type)) && require.resolve("../bin/node-scripts/test.docs"), + (!isDebug && /^(ini)?$/.test(type)) && require.resolve("../bin/node-scripts/test.ini"), + (/^(examples)?$/.test(type)) && require.resolve("../bin/node-scripts/test.examples"), + (!isDebug && /^(addons)?$/.test(type)) && require.resolve("../bin/node-scripts/test.addons"), + (!isDebug && /^(modules)?$/.test(type)) && require.resolve("../bin/node-scripts/test.modules"), + ].forEach(function(filepath) { + filepath && mocha.addFile(filepath); + }) + + mocha.run(function(failures) { + resolve(failures); + }); + }); +} diff --git a/addon-sdk/source/bin/node-scripts/apply-patch.js b/addon-sdk/source/bin/node-scripts/apply-patch.js new file mode 100644 index 000000000..31fbf7d31 --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/apply-patch.js @@ -0,0 +1,64 @@ +/* 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 path = require("path"); +var cp = require("child_process"); +var fs = require("fs"); +var Promise = require("promise"); +var patcher = require("patch-editor"); +var readParam = require("./utils").readParam; + +var isKeeper = /\/addon-sdk\/source/; + +function apply(options) { + return clean(options).then(function() { + return new Promise(function(resolve) { + var patch = path.resolve(readParam("patch")); + var proc = cp.spawn("git", ["apply", patch]); + proc.stdout.pipe(process.stdout); + proc.stderr.pipe(process.stderr); + proc.on("close", resolve); + }); + }); +} +exports.apply = apply; + +function clean(options) { + return new Promise(function(resolve) { + var patch = path.resolve(readParam("patch")); + if (!patch) { + throw new Error("no --patch was provided."); + } + console.log("Cleaning patch " + patch); + + patcher.getChunks({ patch: patch }).then(function(chunks) { + var keepers = []; + + for (var i = chunks.length - 1; i >= 0; i--) { + var chunk = chunks[i]; + var files = chunk.getFilesChanged(); + + // check if the file changed is related to the addon-sdk/source directory + var keepIt = files.map(function(file) { + return (isKeeper.test(file)); + }).reduce(function(prev, curr) { + return prev || curr; + }, false); + + if (keepIt) { + keepers.push(chunk); + } + } + + var contents = "\n" + keepers.join("\n") + "\n"; + contents = contents.replace(/\/addon-sdk\/source/g, ""); + + fs.writeFileSync(patch, contents, { encoding: "utf8" }); + + console.log("Done cleaning patch."); + }).then(resolve).catch(console.error); + }); +} +exports.clean = clean; 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 000000000..dc7c6dfce --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/test.addons.js @@ -0,0 +1,57 @@ +/* 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 isDebug = utils.isDebug; + +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-properties|simple-prefs|page-mod-debugger)/.test(file)) { + return false; + } + + // filter additional add-ons when using debug builds + if (isDebug) { + if (/^(chrome|e10s)/.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.docs.js b/addon-sdk/source/bin/node-scripts/test.docs.js new file mode 100644 index 000000000..e6aef516d --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/test.docs.js @@ -0,0 +1,145 @@ +/* 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 createHash = require('crypto').createHash; +var fs = require("fs"); +var fsExtra = require("fs-extra") +var path = require("path"); +var Promise = require("promise"); +var chai = require("chai"); +var expect = chai.expect; +var teacher = require("teacher"); + +var rootURI = path.join(__dirname, "..", ".."); + +// get a list of words that fail spell check but are still acceptable +var NEW_WORDS = fs.readFileSync(path.join(__dirname, "words.txt")).toString().trim().split("\n"); + +var CACHE_PATH = path.join(__dirname, "..", "..", "cache", "spellchecks.json"); + +var CACHE = {}; + +try { + CACHE = JSON.parse(fs.readFileSync(CACHE_PATH).toString()); +} +catch (e) {} + +function md5(str) { + return createHash("md5").update(str).digest("utf8"); +} + +function addCacheHash(hash) { + CACHE[hash] = true; + fsExtra.ensureFileSync(CACHE_PATH); + fsExtra.writeJSONSync(CACHE_PATH, CACHE); +} + +describe("Spell Checking", function () { + it("Spellcheck CONTRIBUTING.md", function (done) { + var readme = path.join(rootURI, "CONTRIBUTING.md"); + + fs.readFile(readme, function (err, data) { + if (err) { + throw err; + } + var text = data.toString(); + var hash = md5(text); + + // skip this test if we know we have done the + // exact same test with positive results before + if (CACHE[hash]) { + expect(CACHE[hash]).to.be.equal(true); + return done(); + } + + teacher.check(text, function(err, data) { + expect(err).to.be.equal(null); + + var results = data || []; + results = results.filter(function(result) { + if (NEW_WORDS.indexOf(result.string.toLowerCase()) != -1) { + return false; + } + + // ignore anything that starts with a dash + if (result.string[0] == "-") { + return false; + } + + if (!(new RegExp(result.string)).test(text)) { + return false; + } + + return true; + }) + + if (results.length > 0) { + console.log(results); + } + else { + addCacheHash(hash); + } + + expect(results.length).to.be.equal(0); + + setTimeout(done, 500); + }); + }); + }); + + it("Spellcheck README.md", function (done) { + var readme = path.join(rootURI, "README.md"); + + fs.readFile(readme, function (err, data) { + if (err) { + throw err; + } + var text = data.toString(); + var hash = md5(text); + + // skip this test if we know we have done the + // exact same test with positive results before + if (CACHE[hash]) { + expect(CACHE[hash]).to.be.equal(true); + return done(); + } + + teacher.check(text, function(err, data) { + expect(err).to.be.equal(null); + + var results = data || []; + results = results.filter(function(result) { + if (NEW_WORDS.indexOf(result.string.toLowerCase()) != -1) { + return false; + } + + // ignore anything that starts with a dash + if (result.string[0] == "-") { + return false; + } + + // ignore anything that we don't find in the original text, + // for some reason "bootstrap.js" becomes "bootstrapjs". + if (!(new RegExp(result.string)).test(text)) { + return false; + } + + return true; + }) + + if (results.length > 0) { + console.log(results); + } + else { + addCacheHash(hash); + } + + expect(results.length).to.be.equal(0); + + done(); + }); + }); + }); +}); 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 000000000..71f7ee43c --- /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.firefox-bin.js b/addon-sdk/source/bin/node-scripts/test.firefox-bin.js new file mode 100644 index 000000000..2570dae20 --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/test.firefox-bin.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"; + +var fs = require("fs"); +var Promise = require("promise"); +var chai = require("chai"); +var expect = chai.expect; +var normalizeBinary = require("fx-runner/lib/utils").normalizeBinary; + +//var firefox_binary = process.env["JPM_FIREFOX_BINARY"] || normalizeBinary("nightly"); + +describe("Checking Firefox binary", function () { + + it("using matching fx-runner version with jpm", function () { + var sdkPackageJSON = require("../../package.json"); + var jpmPackageINI = require("jpm/package.json"); + expect(sdkPackageJSON.devDependencies["fx-runner"]).to.be.equal(jpmPackageINI.dependencies["fx-runner"]); + }); + + it("exists", function (done) { + var useEnvVar = new Promise(function(resolve) { + resolve(process.env["JPM_FIREFOX_BINARY"]); + }); + + var firefox_binary = process.env["JPM_FIREFOX_BINARY"] ? useEnvVar : normalizeBinary("nightly"); + firefox_binary.then(function(path) { + expect(path).to.be.ok; + fs.exists(path, function (exists) { + expect(exists).to.be.ok; + done(); + }); + }) + }); + +}); diff --git a/addon-sdk/source/bin/node-scripts/test.ini.js b/addon-sdk/source/bin/node-scripts/test.ini.js new file mode 100644 index 000000000..07bd15d1f --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/test.ini.js @@ -0,0 +1,68 @@ +/* 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 fs = require("fs"); +var path = require("path"); +var Promise = require("promise"); +var chai = require("chai"); +var expect = chai.expect; +var ini = require("./update-ini"); + +var addonINI = path.resolve("./test/addons/jetpack-addon.ini"); +var packageINI = path.resolve("./test/jetpack-package.ini"); + +describe("Checking ini files", function () { + + it("Check test/addons/jetpack-addon.ini", function (done) { + + fs.readFile(addonINI, function (err, data) { + if (err) { + throw err; + } + // filter comments + var text = data.toString().split("\n").filter(function(line) { + return !/^\s*#/.test(line); + }).join("\n"); + var expected = ""; + + ini.makeAddonIniContent() + .then(function(contents) { + expected = contents; + + setTimeout(function end() { + expect(text.trim()).to.be.equal(expected.trim()); + done(); + }); + }); + }); + + }); + + it("Check test/jetpack-package.ini", function (done) { + + fs.readFile(packageINI, function (err, data) { + if (err) { + throw err; + } + // filter comments + var text = data.toString().split("\n").filter(function(line) { + return !/^\s*#/.test(line); + }).join("\n"); + var expected = ""; + + ini.makePackageIniContent() + .then(function(contents) { + expected = contents; + + setTimeout(function end() { + expect(text.trim()).to.be.equal(expected.trim()); + done(); + }); + }); + }); + + }); + +}); 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 000000000..eb400a5f3 --- /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/update-ini.js b/addon-sdk/source/bin/node-scripts/update-ini.js new file mode 100644 index 000000000..634cbc1de --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/update-ini.js @@ -0,0 +1,141 @@ +/* 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 path = require("path"); +var cp = require("child_process"); +var fs = require("fs"); +var Promise = require("promise"); +var parser = require("ini-parser"); + +var addonINI = path.resolve("./test/addons/jetpack-addon.ini"); +var addonsDir = path.resolve("./test/addons/"); +var packageINI = path.resolve("./test/jetpack-package.ini"); +var packageDir = path.resolve("./test/"); +var packageIgnorables = [ "addons", "preferences" ]; +var packageSupportFiles = [ + "fixtures.js", + "test-context-menu.html", + "util.js" +] + +function updateAddonINI() { + return new Promise(function(resolve) { + console.log("Start updating " + addonINI); + + makeAddonIniContent(). + then(function(contents) { + fs.writeFileSync(addonINI, contents, { encoding: "utf8" }); + console.log("Done updating " + addonINI); + resolve(); + }); + }) +} +exports.updateAddonINI = updateAddonINI; + +function makeAddonIniContent() { + return new Promise(function(resolve) { + var data = parser.parse(fs.readFileSync(addonINI, { encoding: "utf8" }).toString()); + var result = {}; + + fs.readdir(addonsDir, function(err, files) { + // get a list of folders + var folders = files.filter(function(file) { + return fs.statSync(path.resolve(addonsDir, file)).isDirectory(); + }).sort(); + + // copy any related data from the existing ini + folders.forEach(function(folder) { + var oldData = data[folder + ".xpi"]; + result[folder] = oldData ? oldData : {}; + }); + + // build a new ini file + var contents = []; + Object.keys(result).sort().forEach(function(key) { + contents.push("[" + key + ".xpi]"); + Object.keys(result[key]).forEach(function(dataKey) { + contents.push(dataKey + " = " + result[key][dataKey]); + }); + }); + contents = contents.join("\n") + "\n"; + + return resolve(contents); + }); + }); +} +exports.makeAddonIniContent = makeAddonIniContent; + +function makePackageIniContent() { + return new Promise(function(resolve) { + var data = parser.parse(fs.readFileSync(packageINI, { encoding: "utf8" }).toString()); + var result = {}; + + fs.readdir(packageDir, function(err, files) { + // get a list of folders + var folders = files.filter(function(file) { + var ignore = (packageIgnorables.indexOf(file) >= 0); + var isDir = fs.statSync(path.resolve(packageDir, file)).isDirectory(); + return (isDir && !ignore); + }).sort(); + + // get a list of "test-"" files + var files = files.filter(function(file) { + var ignore = !/^test\-.*\.js$/i.test(file); + var isDir = fs.statSync(path.resolve(packageDir, file)).isDirectory(); + return (!isDir && !ignore); + }).sort(); + + // get a list of the support files + var support_files = packageSupportFiles.map(function(file) { + return " " + file; + }); + folders.forEach(function(folder) { + support_files.push(" " + folder + "/**"); + }); + support_files = support_files.sort(); + + // copy any related data from the existing ini + files.forEach(function(file) { + var oldData = data[file]; + result[file] = oldData ? oldData : {}; + }); + + // build a new ini file + var contents = [ + "[DEFAULT]", + "support-files =" + ]; + support_files.forEach(function(support_file) { + contents.push(support_file); + }); + contents.push(""); + + Object.keys(result).sort().forEach(function(key) { + contents.push("[" + key + "]"); + Object.keys(result[key]).forEach(function(dataKey) { + contents.push(dataKey + " = " + result[key][dataKey]); + }); + }); + contents = contents.join("\n") + "\n"; + + return resolve(contents); + }); + }); +} +exports.makePackageIniContent = makePackageIniContent; + +function updatePackageINI() { + return new Promise(function(resolve) { + console.log("Start updating " + packageINI); + + makeAddonIniContent(). + then(function(contents) { + fs.writeFileSync(packageINI, contents, { encoding: "utf8" }); + console.log("Done updating " + packageINI); + resolve(); + }); + }) +} +exports.updatePackageINI = updatePackageINI; 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 000000000..1d7f94474 --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/utils.js @@ -0,0 +1,104 @@ +/* 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"); + +var OUTPUT_FILTERS = [ + /[^\n\r]+WARNING\: NS_ENSURE_SUCCESS\(rv, rv\) failed[^\n]+\n\r?/ +]; + +var isDebug = (process.env["JPM_FX_DEBUG"] == "1"); +exports.isDebug = isDebug; + +function spawn (cmd, options) { + options = options || {}; + var env = _.extend({}, options.env, process.env); + + if (isDebug) { + env["MOZ_QUIET"] = 1; + } + + var e10s = options.e10s || false; + + return child_process.spawn("node", [ + jpm, cmd, "-v", "--tbpl", + "--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) { + for (var i = OUTPUT_FILTERS.length - 1; i >= 0; i--) { + if (OUTPUT_FILTERS[i].test(data)) { + return null; + } + } + output.push(data); + return null; + }); + + if (p) { + proc.stdout.pipe(p.stdout); + } + else if (!isDebug) { + proc.stdout.pipe(DEFAULT_PROCESS.stdout); + } + else { + proc.stdout.on("data", function (data) { + data = (data || "") + ""; + if (/TEST-/.test(data)) { + DEFAULT_PROCESS.stdout.write(data.replace(/[\s\n]+$/, "") + "\n"); + } + }); + } + + proc.on("close", function(code) { + var out = output.join(""); + var buildDisplayed = /Build \d+/.test(out); + 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(buildDisplayed).to.equal(true); + 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/bin/node-scripts/words.txt b/addon-sdk/source/bin/node-scripts/words.txt new file mode 100644 index 000000000..b5b29f74b --- /dev/null +++ b/addon-sdk/source/bin/node-scripts/words.txt @@ -0,0 +1,11 @@ +addon-sdk +github +stackoverflow +bugzilla +irc +jsantell +mossop +gozala +zer0 +autonome +0c0w3 diff --git a/addon-sdk/source/examples/actor-repl/README.md b/addon-sdk/source/examples/actor-repl/README.md new file mode 100644 index 000000000..5228719e9 --- /dev/null +++ b/addon-sdk/source/examples/actor-repl/README.md @@ -0,0 +1,3 @@ +# Actor REPL + +Simple REPL for a Firefox debugging protocol. diff --git a/addon-sdk/source/examples/actor-repl/data/codemirror-compressed.js b/addon-sdk/source/examples/actor-repl/data/codemirror-compressed.js new file mode 100644 index 000000000..6d8e4bf71 --- /dev/null +++ b/addon-sdk/source/examples/actor-repl/data/codemirror-compressed.js @@ -0,0 +1,5 @@ +window.CodeMirror=function(){"use strict";function z(a,c){if(!(this instanceof z))return new z(a,c);this.options=c=c||{};for(var d in fd)!c.hasOwnProperty(d)&&fd.hasOwnProperty(d)&&(c[d]=fd[d]);M(c);var e="string"==typeof c.value?0:c.value.first,f=this.display=A(a,e);f.wrapper.CodeMirror=this,J(this),c.autofocus&&!r&&Qb(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,focused:!1,suppressEdits:!1,pasteIncoming:!1,cutIncoming:!1,draggingText:!1,highlight:new jf},H(this),c.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap");var g=c.value;"string"==typeof g&&(g=new te(c.value,c.mode)),Ib(this,xe)(this,g),b&&setTimeout(tf(Pb,this,!0),20),Tb(this);var h;try{h=document.activeElement==f.input}catch(i){}h||c.autofocus&&!r?setTimeout(tf(rc,this),20):sc(this),Ib(this,function(){for(var a in ed)ed.propertyIsEnumerable(a)&&ed[a](this,c[a],hd);for(var b=0;bb.maxLineLength&&(b.maxLineLength=d,b.maxLine=a)})}function M(a){var b=pf(a.gutters,"CodeMirror-linenumbers");-1==b&&a.lineNumbers?a.gutters=a.gutters.concat(["CodeMirror-linenumbers"]):b>-1&&!a.lineNumbers&&(a.gutters=a.gutters.slice(0),a.gutters.splice(b,1))}function N(a){var b=a.display,c=a.doc.height,d=c+jb(b);b.sizer.style.minHeight=b.heightForcer.style.top=d+"px",b.gutters.style.height=Math.max(d,b.scroller.clientHeight-gf)+"px";var e=Math.max(d,b.scroller.scrollHeight),f=b.scroller.scrollWidth>b.scroller.clientWidth+1,g=e>b.scroller.clientHeight+1;g?(b.scrollbarV.style.display="block",b.scrollbarV.style.bottom=f?Hf(b.measure)+"px":"0",b.scrollbarV.firstChild.style.height=Math.max(0,e-b.scroller.clientHeight+b.scrollbarV.clientHeight)+"px"):(b.scrollbarV.style.display="",b.scrollbarV.firstChild.style.height="0"),f?(b.scrollbarH.style.display="block",b.scrollbarH.style.right=g?Hf(b.measure)+"px":"0",b.scrollbarH.firstChild.style.width=b.scroller.scrollWidth-b.scroller.clientWidth+b.scrollbarH.clientWidth+"px"):(b.scrollbarH.style.display="",b.scrollbarH.firstChild.style.width="0"),f&&g?(b.scrollbarFiller.style.display="block",b.scrollbarFiller.style.height=b.scrollbarFiller.style.width=Hf(b.measure)+"px"):b.scrollbarFiller.style.display="",f&&a.options.coverGutterNextToScrollbar&&a.options.fixedGutter?(b.gutterFiller.style.display="block",b.gutterFiller.style.height=Hf(b.measure)+"px",b.gutterFiller.style.width=b.gutters.offsetWidth+"px"):b.gutterFiller.style.display="",n&&0===Hf(b.measure)&&(b.scrollbarV.style.minWidth=b.scrollbarH.style.minHeight=o?"18px":"12px",b.scrollbarV.style.pointerEvents=b.scrollbarH.style.pointerEvents="none")}function O(a,b,c){var d=a.scroller.scrollTop,e=a.wrapper.clientHeight;"number"==typeof c?d=c:c&&(d=c.top,e=c.bottom-c.top),d=Math.floor(d-ib(a));var f=Math.ceil(d+e);return{from:De(b,d),to:De(b,f)}}function P(a){var b=a.display;if(b.alignWidgets||b.gutters.firstChild&&a.options.fixedGutter){for(var c=S(b)-b.scroller.scrollLeft+a.doc.scrollLeft,d=b.gutters.offsetWidth,e=c+"px",f=b.lineDiv.firstChild;f;f=f.nextSibling)if(f.alignable)for(var g=0,h=f.alignable;g=a.display.showingFrom&&h.to<=a.display.showingTo)break}return g&&(bf(a,"update",a),(a.display.showingFrom!=e||a.display.showingTo!=f)&&bf(a,"viewportChange",a,a.display.showingFrom,a.display.showingTo)),g}function U(a,b,c,d){var e=a.display,f=a.doc;if(!e.wrapper.offsetWidth)return e.showingFrom=e.showingTo=f.first,e.viewOffset=0,void 0;if(!(!d&&0==b.length&&c.from>e.showingFrom&&c.tol&&e.showingTo-l<20&&(l=Math.min(j,e.showingTo)),y)for(k=Ce(Rd(f,ye(f,k)));j>l&&Sd(f,ye(f,l));)++l;var m=[{from:Math.max(e.showingFrom,f.first),to:Math.min(e.showingTo,j)}];if(m=m[0].from>=m[0].to?[]:X(m,b),y)for(var i=0;in.from)){m.splice(i--,1);break}n.to=p}for(var q=0,i=0;il&&(n.to=l),n.from>=n.to?m.splice(i--,1):q+=n.to-n.from}if(!d&&q==l-k&&k==e.showingFrom&&l==e.showingTo)return W(a),void 0;m.sort(function(a,b){return a.from-b.from});try{var r=document.activeElement}catch(s){}.7*(l-k)>q&&(e.lineDiv.style.display="none"),Z(a,k,l,m,h),e.lineDiv.style.display="",r&&document.activeElement!=r&&r.offsetHeight&&r.focus();var t=k!=e.showingFrom||l!=e.showingTo||e.lastSizeC!=e.wrapper.clientHeight;return t&&(e.lastSizeC=e.wrapper.clientHeight,eb(a,400)),e.showingFrom=k,e.showingTo=l,e.gutters.style.height="",V(a),W(a),!0}}function V(a){for(var f,b=a.display,d=b.lineDiv.offsetTop,e=b.lineDiv.firstChild;e;e=e.nextSibling)if(e.lineObj){if(c){var g=e.offsetTop+e.offsetHeight;f=g-d,d=g}else{var h=Df(e);f=h.bottom-h.top}var i=e.lineObj.height-f;if(2>f&&(f=Db(b)),i>.001||-.001>i){Be(e.lineObj,f);var j=e.lineObj.widgets;if(j)for(var k=0;kc;++c){for(var e=b[c],f=[],g=e.diff||0,h=0,i=a.length;i>h;++h){var j=a[h];e.to<=j.from&&e.diff?f.push({from:j.from+g,to:j.to+g}):e.to<=j.from||e.from>=j.to?f.push(j):(e.from>j.from&&f.push({from:j.from,to:e.from}),e.ton){for(;k.lineObj!=b;)k=l(k);i&&n>=e&&k.lineNumber&&Cf(k.lineNumber,R(a.options,n)),k=k.nextSibling}else{if(b.widgets)for(var s,q=0,r=k;r&&20>q;++q,r=r.nextSibling)if(r.lineObj==b&&/div/i.test(r.nodeName)){s=r;break}var t=$(a,b,n,f,s);if(t!=s)j.insertBefore(t,k);else{for(;k!=s;)k=l(k);k=k.nextSibling}t.lineObj=b}++n});k;)k=l(k)}function $(a,b,d,e,f){var k,g=ie(a,b),h=g.pre,i=b.gutterMarkers,j=a.display,l=g.bgClass?g.bgClass+" "+(b.bgClass||""):b.bgClass;if(!(a.options.lineNumbers||i||l||b.wrapClass||b.widgets))return h;if(f){f.alignable=null;for(var q,m=!0,n=0,o=null,p=f.firstChild;p;p=q)if(q=p.nextSibling,/\bCodeMirror-linewidget\b/.test(p.className)){for(var r=0;rb&&(b=0),e.appendChild(zf("div",null,"CodeMirror-selected","position: absolute; left: "+a+"px; top: "+b+"px; width: "+(null==c?h-a:c)+"px; height: "+(d-b)+"px"))}function j(b,d,e){function m(c,d){return xb(a,Gc(b,c),"div",f,d)}var k,l,f=ye(c,b),j=f.text.length;return Of(Fe(f),d||0,null==e?j:e,function(a,b,c){var n,o,p,f=m(a,"left");if(a==b)n=f,o=p=f.left;else{if(n=m(b-1,"right"),"rtl"==c){var q=f;f=n,n=q}o=f.left,p=n.right}null==d&&0==a&&(o=g),n.top-f.top>3&&(i(o,f.top,null,f.bottom),o=g,f.bottoml.bottom||n.bottom==l.bottom&&n.right>l.right)&&(l=n),g+1>o&&(o=g),i(o,n.top,p-o,n.bottom)}),{start:k,end:l}}var b=a.display,c=a.doc,d=a.doc.sel,e=document.createDocumentFragment(),f=kb(a.display),g=f.left,h=b.lineSpace.offsetWidth-f.right;if(d.from.line==d.to.line)j(d.from.line,d.from.ch,d.to.ch);else{var k=ye(c,d.from.line),l=ye(c,d.to.line),m=Rd(c,k)==Rd(c,l),n=j(d.from.line,d.from.ch,m?k.text.length:null).end,o=j(d.to.line,m?0:null,d.to.ch).start;m&&(n.top0&&(b.blinker=setInterval(function(){b.cursor.style.visibility=b.otherCursor.style.visibility=(c=!c)?"":"hidden"},a.options.cursorBlinkRate))}}function eb(a,b){a.doc.mode.startState&&a.doc.frontier=a.display.showingTo)){var f,c=+new Date+a.options.workTime,d=nd(b.mode,hb(a,b.frontier)),e=[];b.iter(b.frontier,Math.min(b.first+b.size,a.display.showingTo+500),function(g){if(b.frontier>=a.display.showingFrom){var h=g.styles;g.styles=ce(a,g,d,!0);for(var i=!h||h.length!=g.styles.length,j=0;!i&&jc?(eb(a,a.options.workDelay),!0):void 0}),e.length&&Ib(a,function(){for(var a=0;ag;--h){if(h<=f.first)return f.first;var i=ye(f,h-1);if(i.stateAfter&&(!c||h<=f.frontier))return h;var j=kf(i.text,null,a.options.tabSize);(null==e||d>j)&&(e=h-1,d=j)}return e}function hb(a,b,c){var d=a.doc,e=a.display;if(!d.mode.startState)return!0;var f=gb(a,b,c),g=f>d.first&&ye(d,f-1).stateAfter;return g=g?nd(d.mode,g):od(d.mode),d.iter(f,b,function(c){ee(a,c.text,g);var h=f==b-1||0==f%5||f>=e.showingFrom&&ff&&0==h&&(f=1)}return e=h>c?"left":c>h?"right":e,"left"==e&&i.leftSide?i=i.leftSide:"right"==e&&i.rightSide&&(i=i.rightSide),{left:c>h?i.right:i.left,right:h>c?i.left:i.right,top:i.top,bottom:i.bottom}}function mb(a,b){for(var c=a.display.measureLineCache,d=0;ds&&(c=s),0>b&&(b=0);for(var d=q.length-2;d>=0;d-=2){var e=q[d],f=q[d+1];if(!(e>c||b>f)&&(b>=e&&f>=c||e>=b&&c>=f||Math.min(c,f)-Math.max(b,e)>=c-b>>1)){q[d]=Math.min(b,e),q[d+1]=Math.max(c,f);break}}return 0>d&&(d=q.length,q.push(b,c)),{left:a.left-p.left,right:a.right-p.left,top:d,bottom:null}}function u(a){a.bottom=q[a.top+1],a.top=q[a.top]}if(!a.options.lineWrapping&&e.text.length>=a.options.crudeMeasuringFrom)return qb(a,e);var f=a.display,g=sf(e.text.length),h=ie(a,e,g,!0).pre;if(b&&!c&&!a.options.lineWrapping&&h.childNodes.length>100){for(var i=document.createDocumentFragment(),j=10,k=h.childNodes.length,l=0,m=Math.ceil(k/j);m>l;++l){for(var n=zf("div",null,null,"display: inline-block"),o=0;j>o&&k;++o)n.appendChild(h.firstChild),--k;i.appendChild(n)}h.appendChild(i)}Bf(f.measure,h);var p=Df(f.lineDiv),q=[],r=sf(e.text.length),s=h.offsetHeight;d&&f.measure.first!=h&&Bf(f.measure,h);for(var v,l=0;l1&&(x=r[l]=t(y[0]),x.rightSide=t(y[y.length-1]))}x||(x=r[l]=t(Df(w))),v.measureRight&&(x.right=Df(v.measureRight).left-p.left),v.leftSide&&(x.leftSide=t(Df(v.leftSide)))}Af(a.display.measure);for(var v,l=0;l=a.options.crudeMeasuringFrom)return lb(a,b,b.text.length,f&&f.measure,"right").right;var g=ie(a,b,null,!0).pre,h=g.appendChild(Jf(a.display.measure));Bf(a.display.measure,g);var i=Df(h);return 0==i.right&&0==i.bottom&&(h=g.appendChild(zf("span","\xa0")),i=Df(h)),i.left-Df(a.display.lineDiv).left}function sb(a){a.display.measureLineCache.length=a.display.measureLineCachePos=0,a.display.cachedCharWidth=a.display.cachedTextHeight=a.display.cachedPaddingH=null,a.options.lineWrapping||(a.display.maxLineChanged=!0),a.display.lineNumChars=null}function tb(){return window.pageXOffset||(document.documentElement||document.body).scrollLeft}function ub(){return window.pageYOffset||(document.documentElement||document.body).scrollTop}function vb(a,b,c,d){if(b.widgets)for(var e=0;ec.from?f(a-1):f(a,d)}d=d||ye(a.doc,b.line),e||(e=ob(a,d));var h=Fe(d),i=b.ch;if(!h)return f(i);var j=Xf(h,i),k=g(i,j);return null!=Wf&&(k.other=g(i,Wf)),k}function zb(a,b,c,d){var e=new Gc(a,b);return e.xRel=d,c&&(e.outside=!0),e}function Ab(a,b,c){var d=a.doc;if(c+=a.display.viewOffset,0>c)return zb(d.first,0,!0,-1);var e=De(d,c),f=d.first+d.size-1;if(e>f)return zb(d.first+d.size-1,ye(d,f).text.length,!0,1);for(0>b&&(b=0);;){var g=ye(d,e),h=Bb(a,g,e,b,c),i=Pd(g),j=i&&i.find();if(!i||!(h.ch>j.from.ch||h.ch==j.from.ch&&h.xRel>0))return h;e=j.to.line}}function Bb(a,b,c,d,e){function j(d){var e=yb(a,Gc(c,d),"line",b,i);return g=!0,f>e.bottom?e.left-h:fq)return zb(c,n,r,1);for(;;){if(k?n==m||n==Zf(b,m,1):1>=n-m){for(var s=o>d||q-d>=d-o?m:n,t=d-(s==m?o:q);yf(b.text.charAt(s));)++s;var u=zb(c,s,s==m?p:r,0>t?-1:t?1:0);return u}var v=Math.ceil(l/2),w=m+v;if(k){w=m;for(var x=0;v>x;++x)w=Zf(b,w,1)}var y=j(w);y>d?(n=w,q=y,(r=g)&&(q+=1e3),l=v):(m=w,o=y,p=g,l-=v)}}function Db(a){if(null!=a.cachedTextHeight)return a.cachedTextHeight;if(null==Cb){Cb=zf("pre");for(var b=0;49>b;++b)Cb.appendChild(document.createTextNode("x")),Cb.appendChild(zf("br"));Cb.appendChild(document.createTextNode("x"))}Bf(a.measure,Cb);var c=Cb.offsetHeight/50;return c>3&&(a.cachedTextHeight=c),Af(a.measure),c||1}function Eb(a){if(null!=a.cachedCharWidth)return a.cachedCharWidth;var b=zf("span","x"),c=zf("pre",[b]);Bf(a.measure,c);var d=b.offsetWidth;return d>2&&(a.cachedCharWidth=d),d||10}function Gb(a){a.curOp={changes:[],forceUpdate:!1,updateInput:null,userSelChange:null,textChanged:null,selectionChanged:!1,cursorActivity:!1,updateMaxLine:!1,updateScrollPos:!1,id:++Fb},af++||(_e=[])}function Hb(a){var b=a.curOp,c=a.doc,d=a.display;if(a.curOp=null,b.updateMaxLine&&L(a),d.maxLineChanged&&!a.options.lineWrapping&&d.maxLine){var e=rb(a,d.maxLine);d.sizer.style.minWidth=Math.max(0,e+3)+"px",d.maxLineChanged=!1;var f=Math.max(0,d.sizer.offsetLeft+d.sizer.offsetWidth-d.scroller.clientWidth);fj&&c.charCodeAt(j)==h.charCodeAt(j);)++j;var l=f.from,m=f.to,n=h.slice(j);j-1){$c(a,f.head.line,"smart");break}}return h.length>1e3||h.indexOf("\n")>-1?b.value=a.display.prevInput="":a.display.prevInput=h,i&&Hb(a),a.state.pasteIncoming=a.state.cutIncoming=!1,!0}function Pb(a,b){var c,e,f=a.doc;if(Hc(f.sel.from,f.sel.to))b&&(a.display.prevInput=a.display.input.value="",g&&!d&&(a.display.inputHasSelection=null));else{a.display.prevInput="",c=Mf&&(f.sel.to.line-f.sel.from.line>100||(e=a.getSelection()).length>1e3);var h=c?"-":e||a.getSelection();a.display.input.value=h,a.state.focused&&of(a.display.input),g&&!d&&(a.display.inputHasSelection=h)}a.display.inaccurateSelection=c}function Qb(a){"nocursor"==a.options.readOnly||r&&document.activeElement==a.display.input||a.display.input.focus()}function Rb(a){a.state.focused||(Qb(a),rc(a))}function Sb(a){return a.options.readOnly||a.doc.cantEdit}function Tb(a){function e(){a.state.focused&&setTimeout(tf(Qb,a),0)}function i(){null==f&&(f=setTimeout(function(){f=null,c.cachedCharWidth=c.cachedTextHeight=c.cachedPaddingH=Gf=null,sb(a),Kb(a,tf(Lb,a))},100))}function j(){for(var a=c.wrapper.parentNode;a&&a!=document.body;a=a.parentNode);a?setTimeout(j,5e3):Ze(window,"resize",i)}function k(b){cf(a,b)||a.options.onDragEvent&&a.options.onDragEvent(a,Re(b))||Ve(b)}function l(b){c.inaccurateSelection&&(c.prevInput="",c.inaccurateSelection=!1,c.input.value=a.getSelection(),of(c.input)),"cut"==b.type&&(a.state.cutIncoming=!0)}var c=a.display;Ye(c.scroller,"mousedown",Ib(a,Yb)),b?Ye(c.scroller,"dblclick",Ib(a,function(b){if(!cf(a,b)){var c=Vb(a,b);if(c&&!_b(a,b)&&!Ub(a.display,b)){Se(b);var d=cd(ye(a.doc,c.line).text,c);Pc(a.doc,d.from,d.to)}}})):Ye(c.scroller,"dblclick",function(b){cf(a,b)||Se(b)}),Ye(c.lineSpace,"selectstart",function(a){Ub(c,a)||Se(a)}),w||Ye(c.scroller,"contextmenu",function(b){uc(a,b)}),Ye(c.scroller,"scroll",function(){c.scroller.clientHeight&&(dc(a,c.scroller.scrollTop),ec(a,c.scroller.scrollLeft,!0),$e(a,"scroll",a))}),Ye(c.scrollbarV,"scroll",function(){c.scroller.clientHeight&&dc(a,c.scrollbarV.scrollTop)}),Ye(c.scrollbarH,"scroll",function(){c.scroller.clientHeight&&ec(a,c.scrollbarH.scrollLeft)}),Ye(c.scroller,"mousewheel",function(b){hc(a,b)}),Ye(c.scroller,"DOMMouseScroll",function(b){hc(a,b)}),Ye(c.scrollbarH,"mousedown",e),Ye(c.scrollbarV,"mousedown",e),Ye(c.wrapper,"scroll",function(){c.wrapper.scrollTop=c.wrapper.scrollLeft=0});var f;Ye(window,"resize",i),setTimeout(j,5e3),Ye(c.input,"keyup",Ib(a,nc)),Ye(c.input,"input",function(){g&&!d&&a.display.inputHasSelection&&(a.display.inputHasSelection=null),Nb(a)}),Ye(c.input,"keydown",Ib(a,pc)),Ye(c.input,"keypress",Ib(a,qc)),Ye(c.input,"focus",tf(rc,a)),Ye(c.input,"blur",tf(sc,a)),a.options.dragDrop&&(Ye(c.scroller,"dragstart",function(b){cc(a,b)}),Ye(c.scroller,"dragenter",k),Ye(c.scroller,"dragover",k),Ye(c.scroller,"drop",Ib(a,bc))),Ye(c.scroller,"paste",function(b){Ub(c,b)||(Qb(a),Nb(a))}),Ye(c.input,"paste",function(){if(h&&!a.state.fakedLastChar&&!(new Date-a.state.lastMiddleDown<200)){var b=c.input.selectionStart,d=c.input.selectionEnd; +c.input.value+="$",c.input.selectionStart=b,c.input.selectionEnd=d,a.state.fakedLastChar=!0}a.state.pasteIncoming=!0,Nb(a)}),Ye(c.input,"cut",l),Ye(c.input,"copy",l),m&&Ye(c.sizer,"mouseup",function(){document.activeElement==c.input&&c.input.blur(),Qb(a)})}function Ub(a,b){for(var c=We(b);c!=a.wrapper;c=c.parentNode)if(!c||c.ignoreEvents||c.parentNode==a.sizer&&c!=a.mover)return!0}function Vb(a,b,c){var d=a.display;if(!c){var e=We(b);if(e==d.scrollbarH||e==d.scrollbarH.firstChild||e==d.scrollbarV||e==d.scrollbarV.firstChild||e==d.scrollbarFiller||e==d.gutterFiller)return null}var f,g,h=Df(d.lineSpace);try{f=b.clientX,g=b.clientY}catch(b){return null}return Ab(a,f-h.left,g-h.top)}function Yb(a){function t(a){if(!Hc(s,a)){if(s=a,"single"==m)return Pc(c.doc,Mc(i,k),a),void 0;if(q=Mc(i,q),r=Mc(i,r),"double"==m){var b=cd(ye(i,a.line).text,a);Ic(a,q)?Pc(c.doc,b.from,r):Pc(c.doc,q,b.to)}else"triple"==m&&(Ic(a,q)?Pc(c.doc,r,Mc(i,Gc(a.line,0))):Pc(c.doc,q,Mc(i,Gc(a.line+1,0))))}}function x(a){var b=++v,d=Vb(c,a,!0);if(d)if(Hc(d,o)){var g=a.clientYu.bottom?20:0;g&&setTimeout(Ib(c,function(){v==b&&(f.scroller.scrollTop+=g,x(a))}),50)}else{Rb(c),o=d,t(d);var e=O(f,i);(d.line>=e.to||d.linel-400&&Hc(Xb.pos,k))m="triple",Se(a),setTimeout(tf(Qb,c),20),dd(c,k.line);else if(Wb&&Wb.time>l-400&&Hc(Wb.pos,k)){m="double",Xb={time:l,pos:k},Se(a);var n=cd(ye(i,k.line).text,k);Pc(c.doc,n.from,n.to)}else Wb={time:l,pos:k};var o=k;if(c.options.dragDrop&&Ef&&!Sb(c)&&!Hc(j.from,j.to)&&!Ic(k,j.from)&&!Ic(j.to,k)&&"single"==m){var p=Ib(c,function(e){h&&(f.scroller.draggable=!1),c.state.draggingText=!1,Ze(document,"mouseup",p),Ze(f.scroller,"drop",p),Math.abs(a.clientX-e.clientX)+Math.abs(a.clientY-e.clientY)<10&&(Se(e),Pc(c.doc,k),Qb(c),b&&!d&&setTimeout(function(){document.body.focus(),Qb(c)},20))});return h&&(f.scroller.draggable=!0),c.state.draggingText=p,f.scroller.dragDrop&&f.scroller.dragDrop(),Ye(document,"mouseup",p),Ye(f.scroller,"drop",p),void 0}Se(a),"single"==m&&Pc(c.doc,Mc(i,k));var q=j.from,r=j.to,s=k,u=Df(f.wrapper),v=0,z=Ib(c,function(a){(g&&!e?a.buttons:Xe(a))?x(a):y(a)}),A=Ib(c,y);Ye(document,"mousemove",z),Ye(document,"mouseup",A)}}}function Zb(a,b,c,d,e){try{var f=b.clientX,g=b.clientY}catch(b){return!1}if(f>=Math.floor(Df(a.display.gutters).right))return!1;d&&Se(b);var h=a.display,i=Df(h.lineDiv);if(g>i.bottom||!ef(a,c))return Ue(b);g-=i.top-h.viewOffset;for(var j=0;j=f){var l=De(a.doc,g),m=a.options.gutters[j];return e(a,c,a,l,m,b),Ue(b)}}}function $b(a,b){return ef(a,"gutterContextMenu")?Zb(a,b,"gutterContextMenu",!1,$e):!1}function _b(a,b){return Zb(a,b,"gutterClick",!0,bf)}function bc(a){var b=this;if(!(cf(b,a)||Ub(b.display,a)||b.options.onDragEvent&&b.options.onDragEvent(b,Re(a)))){Se(a),g&&(ac=+new Date);var c=Vb(b,a,!0),d=a.dataTransfer.files;if(c&&!Sb(b))if(d&&d.length&&window.FileReader&&window.File)for(var e=d.length,f=Array(e),h=0,i=function(a,d){var g=new FileReader;g.onload=function(){f[d]=g.result,++h==e&&(c=Mc(b.doc,c),zc(b.doc,{from:c,to:c,text:Kf(f.join("\n")),origin:"paste"},"around"))},g.readAsText(a)},j=0;e>j;++j)i(d[j],j);else{if(b.state.draggingText&&!Ic(c,b.doc.sel.from)&&!Ic(b.doc.sel.to,c))return b.state.draggingText(a),setTimeout(tf(Qb,b),20),void 0;try{var f=a.dataTransfer.getData("Text");if(f){var k=b.doc.sel.from,l=b.doc.sel.to;Rc(b.doc,c,c),b.state.draggingText&&Fc(b.doc,"",k,l,"paste"),b.replaceSelection(f,null,"paste"),Qb(b)}}catch(a){}}}}function cc(a,b){if(g&&(!a.state.draggingText||+new Date-ac<100))return Ve(b),void 0;if(!cf(a,b)&&!Ub(a.display,b)){var c=a.getSelection();if(b.dataTransfer.setData("Text",c),b.dataTransfer.setDragImage&&!l){var d=zf("img",null,null,"position: fixed; left: 0; top: 0;");d.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",k&&(d.width=d.height=1,a.display.wrapper.appendChild(d),d._top=d.offsetTop),b.dataTransfer.setDragImage(d,0,0),k&&d.parentNode.removeChild(d)}}}function dc(b,c){Math.abs(b.doc.scrollTop-c)<2||(b.doc.scrollTop=c,a||T(b,[],c),b.display.scroller.scrollTop!=c&&(b.display.scroller.scrollTop=c),b.display.scrollbarV.scrollTop!=c&&(b.display.scrollbarV.scrollTop=c),a&&T(b,[]),eb(b,100))}function ec(a,b,c){(c?b==a.doc.scrollLeft:Math.abs(a.doc.scrollLeft-b)<2)||(b=Math.min(b,a.display.scroller.scrollWidth-a.display.scroller.clientWidth),a.doc.scrollLeft=b,P(a),a.display.scroller.scrollLeft!=b&&(a.display.scroller.scrollLeft=b),a.display.scrollbarH.scrollLeft!=b&&(a.display.scrollbarH.scrollLeft=b))}function hc(b,c){var d=c.wheelDeltaX,e=c.wheelDeltaY;null==d&&c.detail&&c.axis==c.HORIZONTAL_AXIS&&(d=c.detail),null==e&&c.detail&&c.axis==c.VERTICAL_AXIS?e=c.detail:null==e&&(e=c.wheelDelta);var f=b.display,g=f.scroller;if(d&&g.scrollWidth>g.clientWidth||e&&g.scrollHeight>g.clientHeight){if(e&&s&&h)for(var i=c.target;i!=g;i=i.parentNode)if(i.lineObj){b.display.currentWheelTarget=i;break}if(d&&!a&&!k&&null!=gc)return e&&dc(b,Math.max(0,Math.min(g.scrollTop+e*gc,g.scrollHeight-g.clientHeight))),ec(b,Math.max(0,Math.min(g.scrollLeft+d*gc,g.scrollWidth-g.clientWidth))),Se(c),f.wheelStartX=null,void 0;if(e&&null!=gc){var j=e*gc,l=b.doc.scrollTop,m=l+f.wrapper.clientHeight;0>j?l=Math.max(0,l+j-50):m=Math.min(b.doc.height,m+j+50),T(b,[],{top:l,bottom:m})}20>fc&&(null==f.wheelStartX?(f.wheelStartX=g.scrollLeft,f.wheelStartY=g.scrollTop,f.wheelDX=d,f.wheelDY=e,setTimeout(function(){if(null!=f.wheelStartX){var a=g.scrollLeft-f.wheelStartX,b=g.scrollTop-f.wheelStartY,c=b&&f.wheelDY&&b/f.wheelDY||a&&f.wheelDX&&a/f.wheelDX;f.wheelStartX=f.wheelStartY=null,c&&(gc=(gc*fc+c)/(fc+1),++fc)}},200)):(f.wheelDX+=d,f.wheelDY+=e))}}function ic(a,b,c){if("string"==typeof b&&(b=pd[b],!b))return!1;a.display.pollingFast&&Ob(a)&&(a.display.pollingFast=!1);var d=a.doc,e=d.sel.shift,f=!1;try{Sb(a)&&(a.state.suppressEdits=!0),c&&(d.sel.shift=!1),f=b(a)!=hf}finally{d.sel.shift=e,a.state.suppressEdits=!1}return f}function jc(a){var b=a.state.keyMaps.slice(0);return a.options.extraKeys&&b.push(a.options.extraKeys),b.push(a.options.keyMap),b}function lc(a,b){var c=rd(a.options.keyMap),e=c.auto;clearTimeout(kc),e&&!td(b)&&(kc=setTimeout(function(){rd(a.options.keyMap)==c&&(a.options.keyMap=e.call?e.call(null,a):e,G(a))},50));var f=ud(b,!0),g=!1;if(!f)return!1;var h=jc(a);return g=b.shiftKey?sd("Shift-"+f,h,function(b){return ic(a,b,!0)})||sd(f,h,function(b){return("string"==typeof b?/^go[A-Z]/.test(b):b.motion)?ic(a,b):void 0}):sd(f,h,function(b){return ic(a,b)}),g&&(Se(b),db(a),d&&(b.oldKeyCode=b.keyCode,b.keyCode=0),bf(a,"keyHandled",a,f,b)),g}function mc(a,b,c){var d=sd("'"+c+"'",jc(a),function(b){return ic(a,b,!0)});return d&&(Se(b),db(a),bf(a,"keyHandled",a,"'"+c+"'",b)),d}function nc(a){var b=this;cf(b,a)||b.options.onKeyEvent&&b.options.onKeyEvent(b,Re(a))||16==a.keyCode&&(b.doc.sel.shift=!1)}function pc(a){var c=this;if(Rb(c),!(cf(c,a)||c.options.onKeyEvent&&c.options.onKeyEvent(c,Re(a)))){b&&27==a.keyCode&&(a.returnValue=!1);var d=a.keyCode;c.doc.sel.shift=16==d||a.shiftKey;var e=lc(c,a);k&&(oc=e?d:null,!e&&88==d&&!Mf&&(s?a.metaKey:a.ctrlKey)&&c.replaceSelection(""))}}function qc(a){var b=this;if(!(cf(b,a)||b.options.onKeyEvent&&b.options.onKeyEvent(b,Re(a)))){var c=a.keyCode,e=a.charCode;if(k&&c==oc)return oc=null,Se(a),void 0;if(!(k&&(!a.which||a.which<10)||m)||!lc(b,a)){var f=String.fromCharCode(null==e?c:e);mc(b,a,f)||(g&&!d&&(b.display.inputHasSelection=null),Nb(b))}}}function rc(a){"nocursor"!=a.options.readOnly&&(a.state.focused||($e(a,"focus",a),a.state.focused=!0,-1==a.display.wrapper.className.search(/\bCodeMirror-focused\b/)&&(a.display.wrapper.className+=" CodeMirror-focused"),a.curOp||(Pb(a,!0),h&&setTimeout(tf(Pb,a,!0),0))),Mb(a),db(a))}function sc(a){a.state.focused&&($e(a,"blur",a),a.state.focused=!1,a.display.wrapper.className=a.display.wrapper.className.replace(" CodeMirror-focused","")),clearInterval(a.display.blinker),setTimeout(function(){a.state.focused||(a.doc.sel.shift=!1)},150)}function uc(a,b){function l(){if(null!=c.input.selectionStart){var a=c.input.value="\u200b"+(Hc(e.from,e.to)?"":c.input.value);c.prevInput="\u200b",c.input.selectionStart=1,c.input.selectionEnd=a.length}}function m(){if(c.inputDiv.style.position="relative",c.input.style.cssText=j,d&&(c.scrollbarV.scrollTop=c.scroller.scrollTop=h),Mb(a),null!=c.input.selectionStart){(!g||d)&&l(),clearTimeout(tc);var b=0,e=function(){"\u200b"==c.prevInput&&0==c.input.selectionStart?Ib(a,pd.selectAll)(a):b++<10?tc=setTimeout(e,500):Pb(a)};tc=setTimeout(e,200)}}if(!cf(a,b,"contextmenu")){var c=a.display,e=a.doc.sel;if(!Ub(c,b)&&!$b(a,b)){var f=Vb(a,b),h=c.scroller.scrollTop;if(f&&!k){var i=a.options.resetSelectionOnContextMenu;i&&(Hc(e.from,e.to)||Ic(f,e.from)||!Ic(f,e.to))&&Ib(a,Rc)(a.doc,f,f);var j=c.input.style.cssText;if(c.inputDiv.style.position="absolute",c.input.style.cssText="position: fixed; width: 30px; height: 30px; top: "+(b.clientY-5)+"px; left: "+(b.clientX-5)+"px; z-index: 1000; background: transparent; outline: none;"+"border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);",Qb(a),Pb(a,!0),Hc(e.from,e.to)&&(c.input.value=c.prevInput=" "),g&&!d&&l(),w){Ve(b);var n=function(){Ze(window,"mouseup",n),setTimeout(m,20)};Ye(window,"mouseup",n)}else setTimeout(m,50)}}}}function wc(a,b,c){if(!Ic(b.from,c))return Mc(a,c);var d=b.text.length-1-(b.to.line-b.from.line);if(c.line>b.to.line+d){var e=c.line-d,f=a.first+a.size-1;return e>f?Gc(f,ye(a,f).text.length):Nc(c,ye(a,e).text.length)}if(c.line==b.to.line+d)return Nc(c,nf(b.text).length+(1==b.text.length?b.from.ch:0)+ye(a,b.to.line).text.length-b.to.ch);var g=c.line-b.from.line;return Nc(c,b.text[g].length+(g?0:b.from.ch))}function xc(a,b,c){if(c&&"object"==typeof c)return{anchor:wc(a,b,c.anchor),head:wc(a,b,c.head)};if("start"==c)return{anchor:b.from,head:b.from};var d=vc(b);if("around"==c)return{anchor:b.from,head:d};if("end"==c)return{anchor:d,head:d};var e=function(a){if(Ic(a,b.from))return a;if(!Ic(b.to,a))return d;var c=a.line+b.text.length-(b.to.line-b.from.line)-1,e=a.ch;return a.line==b.to.line&&(e+=d.ch-b.to.ch),Gc(c,e)};return{anchor:e(a.sel.anchor),head:e(a.sel.head)}}function yc(a,b,c){var d={canceled:!1,from:b.from,to:b.to,text:b.text,origin:b.origin,cancel:function(){this.canceled=!0}};return c&&(d.update=function(b,c,d,e){b&&(this.from=Mc(a,b)),c&&(this.to=Mc(a,c)),d&&(this.text=d),void 0!==e&&(this.origin=e)}),$e(a,"beforeChange",a,d),a.cm&&$e(a.cm,"beforeChange",a.cm,d),d.canceled?null:{from:d.from,to:d.to,text:d.text,origin:d.origin}}function zc(a,b,c,d){if(a.cm){if(!a.cm.curOp)return Ib(a.cm,zc)(a,b,c,d);if(a.cm.state.suppressEdits)return}if(!(ef(a,"beforeChange")||a.cm&&ef(a.cm,"beforeChange"))||(b=yc(a,b,!0))){var e=x&&!d&&Jd(a,b.from,b.to);if(e){for(var f=e.length-1;f>=1;--f)Ac(a,{from:e[f].from,to:e[f].to,text:[""]});e.length&&Ac(a,{from:e[0].from,to:e[0].to,text:b.text},c)}else Ac(a,b,c)}}function Ac(a,b,c){if(1!=b.text.length||""!=b.text[0]||!Hc(b.from,b.to)){var d=xc(a,b,c);Je(a,b,d,a.cm?a.cm.curOp.id:0/0),Dc(a,b,d,Gd(a,b));var e=[];we(a,function(a,c){c||-1!=pf(e,a.history)||(Pe(a.history,b),e.push(a.history)),Dc(a,b,null,Gd(a,b))})}}function Bc(a,b){if(!a.cm||!a.cm.state.suppressEdits){var c=a.history,d=("undo"==b?c.done:c.undone).pop();if(d){var e={changes:[],anchorBefore:d.anchorAfter,headBefore:d.headAfter,anchorAfter:d.anchorBefore,headAfter:d.headBefore,generation:c.generation};("undo"==b?c.undone:c.done).push(e),c.generation=d.generation||++c.maxGeneration;for(var f=ef(a,"beforeChange")||a.cm&&ef(a.cm,"beforeChange"),g=d.changes.length-1;g>=0;--g){var h=d.changes[g];if(h.origin=b,f&&!yc(a,h,!1))return("undo"==b?c.done:c.undone).length=0,void 0;e.changes.push(Ie(a,h));var i=g?xc(a,h,null):{anchor:d.anchorBefore,head:d.headBefore};Dc(a,h,i,Id(a,h));var j=[];we(a,function(a,b){b||-1!=pf(j,a.history)||(Pe(a.history,h),j.push(a.history)),Dc(a,h,null,Id(a,h))})}}}}function Cc(a,b){function c(a){return Gc(a.line+b,a.ch)}a.first+=b,a.cm&&Lb(a.cm,a.first,a.first,b),a.sel.head=c(a.sel.head),a.sel.anchor=c(a.sel.anchor),a.sel.from=c(a.sel.from),a.sel.to=c(a.sel.to)}function Dc(a,b,c,d){if(a.cm&&!a.cm.curOp)return Ib(a.cm,Dc)(a,b,c,d);if(b.to.linea.lastLine())){if(b.from.linef&&(b={from:b.from,to:Gc(f,ye(a,f).text.length),text:[b.text[0]],origin:b.origin}),b.removed=ze(a,b.from,b.to),c||(c=xc(a,b,null)),a.cm?Ec(a.cm,b,d,c):pe(a,b,d,c)}}function Ec(a,b,c,d){var e=a.doc,f=a.display,g=b.from,h=b.to,i=!1,j=g.line;a.options.lineWrapping||(j=Ce(Rd(e,ye(e,g.line))),e.iter(j,h.line+1,function(a){return a==f.maxLine?(i=!0,!0):void 0})),Ic(e.sel.head,b.from)||Ic(b.to,e.sel.head)||(a.curOp.cursorActivity=!0),pe(e,b,c,d,E(a)),a.options.lineWrapping||(e.iter(j,g.line+b.text.length,function(a){var b=K(e,a);b>f.maxLineLength&&(f.maxLine=a,f.maxLineLength=b,f.maxLineChanged=!0,i=!1)}),i&&(a.curOp.updateMaxLine=!0)),e.frontier=Math.min(e.frontier,g.line),eb(a,400);var k=b.text.length-(h.line-g.line)-1;if(Lb(a,g.line,h.line+1,k),ef(a,"change")){var l={from:g,to:h,text:b.text,removed:b.removed,origin:b.origin};if(a.curOp.textChanged){for(var m=a.curOp.textChanged;m.next;m=m.next);m.next=l}else a.curOp.textChanged=l}}function Fc(a,b,c,d,e){if(d||(d=c),Ic(d,c)){var f=d;d=c,c=f}"string"==typeof b&&(b=Kf(b)),zc(a,{from:c,to:d,text:b,origin:e},null)}function Gc(a,b){return this instanceof Gc?(this.line=a,this.ch=b,void 0):new Gc(a,b)}function Hc(a,b){return a.line==b.line&&a.ch==b.ch}function Ic(a,b){return a.linec?Gc(c,ye(a,c).text.length):Nc(b,ye(a,b.line).text.length)}function Nc(a,b){var c=a.ch;return null==c||c>b?Gc(a.line,b):0>c?Gc(a.line,0):a}function Oc(a,b){return b>=a.first&&b=f.ch:j.to>f.ch))){if(d&&($e(k,"beforeCursorEnter"),k.explicitlyCleared)){if(h.markedSpans){--i;continue}break}if(!k.atomic)continue;var l=k.find()[0>g?"from":"to"];if(Hc(l,f)&&(l.ch+=g,l.ch<0?l=l.line>a.first?Mc(a,Gc(l.line-1)):null:l.ch>h.text.length&&(l=l.line(window.innerHeight||document.documentElement.clientHeight)&&(e=!1),null!=e&&!p){var f=zf("div","\u200b",null,"position: absolute; top: "+(b.top-c.viewOffset)+"px; height: "+(b.bottom-b.top+gf)+"px; left: "+b.left+"px; width: 2px;");a.display.lineSpace.appendChild(f),f.scrollIntoView(e),a.display.lineSpace.removeChild(f)}}}function Vc(a,b,c,d){for(null==d&&(d=0);;){var e=!1,f=yb(a,b),g=c&&c!=b?yb(a,c):f,h=Xc(a,Math.min(f.left,g.left),Math.min(f.top,g.top)-d,Math.max(f.left,g.left),Math.max(f.bottom,g.bottom)+d),i=a.doc.scrollTop,j=a.doc.scrollLeft;if(null!=h.scrollTop&&(dc(a,h.scrollTop),Math.abs(a.doc.scrollTop-i)>1&&(e=!0)),null!=h.scrollLeft&&(ec(a,h.scrollLeft),Math.abs(a.doc.scrollLeft-j)>1&&(e=!0)),!e)return f}}function Wc(a,b,c,d,e){var f=Xc(a,b,c,d,e);null!=f.scrollTop&&dc(a,f.scrollTop),null!=f.scrollLeft&&ec(a,f.scrollLeft)}function Xc(a,b,c,d,e){var f=a.display,g=Db(a.display);0>c&&(c=0);var h=f.scroller.clientHeight-gf,i=f.scroller.scrollTop,j={},k=a.doc.height+jb(f),l=g>c,m=e>k-g;if(i>c)j.scrollTop=l?0:c;else if(e>i+h){var n=Math.min(c,(m?k:e)-h);n!=i&&(j.scrollTop=n)}var o=f.scroller.clientWidth-gf,p=f.scroller.scrollLeft;b+=f.gutters.offsetWidth,d+=f.gutters.offsetWidth;var q=f.gutters.offsetWidth,r=q+10>b;return p+q>b||r?(r&&(b=0),j.scrollLeft=Math.max(0,b-10-q)):d>o+p-3&&(j.scrollLeft=d+10-o),j}function Yc(a,b,c){a.curOp.updateScrollPos={scrollLeft:null==b?a.doc.scrollLeft:b,scrollTop:null==c?a.doc.scrollTop:c}}function Zc(a,b,c){var d=a.curOp.updateScrollPos||(a.curOp.updateScrollPos={scrollLeft:a.doc.scrollLeft,scrollTop:a.doc.scrollTop}),e=a.display.scroller;d.scrollTop=Math.max(0,Math.min(e.scrollHeight-e.clientHeight,d.scrollTop+c)),d.scrollLeft=Math.max(0,Math.min(e.scrollWidth-e.clientWidth,d.scrollLeft+b))}function $c(a,b,c,d){var f,e=a.doc;null==c&&(c="add"),"smart"==c&&(a.doc.mode.indent?f=hb(a,b):c="prev");var g=a.options.tabSize,h=ye(e,b),i=kf(h.text,null,g);h.stateAfter&&(h.stateAfter=null);var k,j=h.text.match(/^\s*/)[0];if(d||/\S/.test(h.text)){if("smart"==c&&(k=a.doc.mode.indent(f,h.text.slice(j.length),h.text),k==hf)){if(!d)return;c="prev"}}else k=0,c="not";"prev"==c?k=b>e.first?kf(ye(e,b-1).text,null,g):0:"add"==c?k=i+a.options.indentUnit:"subtract"==c?k=i-a.options.indentUnit:"number"==typeof c&&(k=i+c),k=Math.max(0,k);var l="",m=0;if(a.options.indentWithTabs)for(var n=Math.floor(k/g);n;--n)m+=g,l+=" ";k>m&&(l+=mf(k-m)),l!=j?Fc(a.doc,l,Gc(b,0),Gc(b,j.length),"+input"):e.sel.head.line==b&&e.sel.head.ch=a.first+a.size?j=!1:(f=b,i=ye(a,b))}function l(a){var b=(e?Zf:$f)(i,g,c,!0);if(null==b){if(a||!k())return j=!1;g=e?(0>c?Sf:Rf)(i):0>c?i.text.length:0}else g=b;return!0}var f=b.line,g=b.ch,h=c,i=ye(a,f),j=!0;if("char"==d)l();else if("column"==d)l(!0);else if("word"==d||"group"==d)for(var m=null,n="group"==d,o=!0;!(0>c)||l(!o);o=!1){var p=i.text.charAt(g)||"\n",q=vf(p)?"w":n&&"\n"==p?"n":!n||/\s/.test(p)?null:"p";if(!n||o||q||(q="s"),m&&m!=q){0>c&&(c=1,l());break}if(q&&(m=q),c>0&&!l(!o))break}var r=Tc(a,Gc(f,g),h,!0);return j||(r.hitSide=!0),r}function bd(a,b,c,d){var g,e=a.doc,f=b.left;if("page"==d){var h=Math.min(a.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight);g=b.top+c*(h-(0>c?1.5:.5)*Db(a.display))}else"line"==d&&(g=c>0?b.bottom+3:b.top-3);for(;;){var i=Ab(a,f,g);if(!i.outside)break;if(0>c?0>=g:g>=e.height){i.hitSide=!0;break}g+=5*c}return i}function cd(a,b){var c=b.ch,d=b.ch;if(a){(b.xRel<0||d==a.length)&&c?--c:++d;for(var e=a.charAt(c),f=vf(e)?vf:/\s/.test(e)?function(a){return/\s/.test(a)}:function(a){return!/\s/.test(a)&&!vf(a)};c>0&&f(a.charAt(c-1));)--c;for(;dg;++g){var i=d(f[g]);if(i)return i}return!1}for(var e=0;e=b:f.to>b);(e||(e=[])).push({from:f.from,to:i?null:f.to,marker:g})}}return e}function Fd(a,b,c){if(a)for(var e,d=0;d=b:f.to>b);if(h||f.from==b&&"bookmark"==g.type&&(!c||f.marker.insertLeft)){var i=null==f.from||(g.inclusiveLeft?f.from<=b:f.from0&&h)for(var l=0;ll;++l)o.push(q);o.push(i)}return o}function Hd(a){for(var b=0;b=0&&0>=l||0>=k&&l>=0)&&(0>=k&&(Jc(j.to,c)||Ld(i.marker)-Kd(e))>0||k>=0&&(Jc(j.from,d)||Kd(i.marker)-Ld(e))<0))return!0}}}function Rd(a,b){for(var c;c=Od(b);)b=ye(a,c.find().from.line);return b}function Sd(a,b){var c=y&&b.markedSpans;if(c)for(var d,e=0;ea.options.maxHighlightLength?(g=!1,f&&ee(a,b,d,j.pos),j.pos=b.length,k=null):k=c.token(j,d),a.options.addModeClass){var l=z.innerMode(c,d).mode.name;l&&(k="m-"+(k?l+" "+k:l))}g&&i==k||(hi;){var d=e[h];d>a&&e.splice(h,1,a,e[h+1],d),h+=2,i=Math.min(a,d)}if(b)if(g.opaque)e.splice(c,h-c,a,b),h=c+2;else for(;h>c;c+=2){var f=e[c+1];e[c+1]=f?f+" "+b:b}})}return e}function de(a,b){return b.styles&&b.styles[0]==a.state.modeGen||(b.styles=ce(a,b,b.stateAfter=hb(a,Ce(b)))),b.styles}function ee(a,b,c,d){var e=a.doc.mode,f=new vd(b,a.options.tabSize);for(f.start=f.pos=d||0,""==b&&e.blankLine&&e.blankLine(c);!f.eol()&&f.pos<=a.options.maxHighlightLength;)e.token(f,c),f.start=f.pos}function he(a,b){if(!a)return null;for(;;){var c=a.match(/(?:^|\s+)line-(background-)?(\S+)/);if(!c)break;a=a.slice(0,c.index)+a.slice(c.index+c[0].length);var d=c[1]?"bgClass":"textClass";null==b[d]?b[d]=c[2]:new RegExp("(?:^|s)"+c[2]+"(?:$|s)").test(b[d])||(b[d]+=" "+c[2])}if(/^\s*$/.test(a))return null;var e=b.cm.options.addModeClass?ge:fe;return e[a]||(e[a]=a.replace(/\S+/g,"cm-$&"))}function ie(a,b,c,d){for(var e,f=b,i=!0;e=Od(f);)f=ye(a.doc,e.find().from.line);var j={pre:zf("pre"),col:0,pos:0,measure:null,measuredSomething:!1,cm:a,copyWidgets:d};do{f.text&&(i=!1),j.measure=f==b&&c,j.pos=0,j.addToken=j.measure?le:ke,(g||h)&&a.getOption("lineWrapping")&&(j.addToken=me(j.addToken));var k=oe(f,j,de(a,f));c&&f==b&&!j.measuredSomething&&(c[0]=j.pre.appendChild(Jf(a.display.measure)),j.measuredSomething=!0),k&&(f=ye(a.doc,k.to.line))}while(k);!c||j.measuredSomething||c[0]||(c[0]=j.pre.appendChild(i?zf("span","\xa0"):Jf(a.display.measure))),j.pre.firstChild||Sd(a.doc,b)||j.pre.appendChild(document.createTextNode("\xa0"));var l;if(c&&g&&(l=Fe(f))){var m=l.length-1;l[m].from==l[m].to&&--m;var n=l[m],o=l[m-1];if(n.from+1==n.to&&o&&n.leveli)?(null!=t.to&&l>t.to&&(l=t.to,n=""),u.className&&(m+=" "+u.className),u.startStyle&&t.from==i&&(o+=" "+u.startStyle),u.endStyle&&t.to==l&&(n+=" "+u.endStyle),u.title&&!p&&(p=u.title),u.collapsed&&(!q||Md(q.marker,u)<0)&&(q=t)):t.from>i&&l>t.from&&(l=t.from),"bookmark"==u.type&&t.from==i&&u.replacedWith&&r.push(u)}if(q&&(q.from||0)==i&&(ne(b,(null==q.to?h:q.to)-i,q.marker,null==q.from),null==q.to))return q.marker.find();if(!q&&r.length)for(var s=0;s=h)break;for(var v=Math.min(h,l);;){if(j){var w=i+j.length;if(!q){var x=w>v?j.slice(0,v-i):j;b.addToken(b,x,k?k+m:m,o,i+x.length==l?n:"",p)}if(w>=v){j=j.slice(v-i),i=v;break}i=w,o=""}j=e.slice(f,f=c[g++]),k=he(c[g++],b)}}else for(var g=1;gp;++p)r.push(new $d(j[p],f(p),e));r.push(new $d(m+k.text.slice(i.ch),n,e)),g(k,k.text.slice(0,h.ch)+j[0],f(0)),a.insert(h.line+1,r)}else if(1==j.length)g(k,k.text.slice(0,h.ch)+j[0]+l.text.slice(i.ch),f(0)),a.remove(h.line+1,o);else{g(k,k.text.slice(0,h.ch)+j[0],f(0)),g(l,m+l.text.slice(i.ch),n);for(var p=1,q=j.length-1,r=[];q>p;++p)r.push(new $d(j[p],f(p),e));o>1&&a.remove(h.line+1,o-1),a.insert(h.line+1,r)}else{for(var p=0,q=j.length-1,r=[];q>p;++p)r.push(new $d(j[p],f(p),e));g(l,l.text,n),o&&a.remove(h.line,o),r.length&&a.insert(h.line,r)}bf(a,"change",a,b),Rc(a,d.anchor,d.head,null,!0)}function qe(a){this.lines=a,this.parent=null;for(var b=0,c=a.length,d=0;c>b;++b)a[b].parent=this,d+=a[b].height;this.height=d}function re(a){this.children=a;for(var b=0,c=0,d=0,e=a.length;e>d;++d){var f=a[d];b+=f.chunkSize(),c+=f.height,f.parent=this}this.size=b,this.height=c,this.parent=null}function we(a,b,c){function d(a,e,f){if(a.linked)for(var g=0;gb){a=d;break}b-=e}return a.lines[b]}function ze(a,b,c){var d=[],e=b.line;return a.iter(b.line,c.line+1,function(a){var f=a.text;e==c.line&&(f=f.slice(0,c.ch)),e==b.line&&(f=f.slice(b.ch)),d.push(f),++e}),d}function Ae(a,b,c){var d=[];return a.iter(b,c,function(a){d.push(a.text)}),d}function Be(a,b){for(var c=b-a.height,d=a;d;d=d.parent)d.height+=c}function Ce(a){if(null==a.parent)return null;for(var b=a.parent,c=pf(b.lines,a),d=b.parent;d;b=d,d=d.parent)for(var e=0;d.children[e]!=b;++e)c+=d.children[e].chunkSize();return c+b.first}function De(a,b){var c=a.first;a:do{for(var d=0,e=a.children.length;e>d;++d){var f=a.children[d],g=f.height;if(g>b){a=f;continue a}b-=g,c+=f.chunkSize()}return c}while(!a.lines);for(var d=0,e=a.lines.length;e>d;++d){var h=a.lines[d],i=h.height;if(i>b)break;b-=i}return c+d}function Ee(a,b){b=Rd(a.doc,b);for(var c=0,d=b.parent,e=0;ef-a.cm.options.historyEventDelay||"*"==b.origin.charAt(0)))){var h=nf(g.changes);Hc(b.from,b.to)&&Hc(b.from,h.to)?h.to=vc(b):g.changes.push(Ie(a,b)),g.anchorAfter=c.anchor,g.headAfter=c.head}else for(g={changes:[Ie(a,b)],generation:e.generation,anchorBefore:a.sel.anchor,headBefore:a.sel.head,anchorAfter:c.anchor,headAfter:c.head},e.done.push(g);e.done.length>e.undoDepth;)e.done.shift();e.generation=++e.maxGeneration,e.lastTime=f,e.lastOp=d,e.lastOrigin=b.origin,h||$e(a,"historyAdded")}function Ke(a){if(!a)return null;for(var c,b=0;b-1&&(nf(g)[k]=i[k],delete i[k])}}return d}function Ne(a,b,c,d){c0}function ff(a){a.prototype.on=function(a,b){Ye(this,a,b)},a.prototype.off=function(a,b){Ze(this,a,b)}}function jf(){this.id=null}function kf(a,b,c,d,e){null==b&&(b=a.search(/[^\s\u00a0]/),-1==b&&(b=a.length));for(var f=d||0,g=e||0;b>f;++f)" "==a.charAt(f)?g+=c-g%c:++g;return g}function mf(a){for(;lf.length<=a;)lf.push(nf(lf)+" ");return lf[a]}function nf(a){return a[a.length-1]}function of(a){if(q)a.selectionStart=0,a.selectionEnd=a.value.length;else try{a.select()}catch(b){}}function pf(a,b){if(a.indexOf)return a.indexOf(b);for(var c=0,d=a.length;d>c;++c)if(a[c]==b)return c;return-1}function qf(a,b){function c(){}c.prototype=a;var d=new c;return b&&rf(b,d),d}function rf(a,b){b||(b={});for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}function sf(a){for(var b=[],c=0;a>c;++c)b.push(void 0);return b}function tf(a){var b=Array.prototype.slice.call(arguments,1);return function(){return a.apply(null,b)}}function vf(a){return/\w/.test(a)||a>"\x80"&&(a.toUpperCase()!=a.toLowerCase()||uf.test(a))}function wf(a){for(var b in a)if(a.hasOwnProperty(b)&&a[b])return!1;return!0}function yf(a){return a.charCodeAt(0)>=768&&xf.test(a)}function zf(a,b,c,d){var e=document.createElement(a);if(c&&(e.className=c),d&&(e.style.cssText=d),"string"==typeof b)Cf(e,b);else if(b)for(var f=0;f0;--b)a.removeChild(a.firstChild);return a}function Bf(a,b){return Af(a).appendChild(b)}function Cf(a,b){d?(a.innerHTML="",a.appendChild(document.createTextNode(b))):a.textContent=b}function Df(a){return a.getBoundingClientRect()}function Ff(){return!1}function Hf(a){if(null!=Gf)return Gf;var b=zf("div",null,null,"width: 50px; height: 50px; overflow-x: scroll");return Bf(a,b),b.offsetWidth&&(Gf=b.offsetHeight-b.clientHeight),Gf||0}function Jf(a){if(null==If){var b=zf("span","\u200b");Bf(a,zf("span",[b,document.createTextNode("x")])),0!=a.firstChild.offsetHeight&&(If=b.offsetWidth<=1&&b.offsetHeight>2&&!c)}return If?zf("span","\u200b"):zf("span","\xa0",null,"display: inline-block; width: 1px; margin-right: -1px")}function Of(a,b,c,d){if(!a)return d(b,c,"ltr");for(var e=!1,f=0;fb||b==c&&g.to==b)&&(d(Math.max(g.from,b),Math.min(g.to,c),1==g.level?"rtl":"ltr"),e=!0)}e||d(b,c,"ltr")}function Pf(a){return a.level%2?a.to:a.from}function Qf(a){return a.level%2?a.from:a.to}function Rf(a){var b=Fe(a);return b?Pf(b[0]):0}function Sf(a){var b=Fe(a);return b?Qf(nf(b)):a.text.length}function Tf(a,b){var c=ye(a.doc,b),d=Rd(a.doc,c);d!=c&&(b=Ce(d));var e=Fe(d),f=e?e[0].level%2?Sf(d):Rf(d):0;return Gc(b,f)}function Uf(a,b){for(var c,d;c=Pd(d=ye(a.doc,b));)b=c.find().to.line;var e=Fe(d),f=e?e[0].level%2?Rf(d):Sf(d):d.text.length;return Gc(b,f)}function Vf(a,b,c){var d=a[0].level;return b==d?!0:c==d?!1:c>b}function Xf(a,b){Wf=null;for(var d,c=0;cb)return c;if(e.from==b||e.to==b){if(null!=d)return Vf(a,e.level,a[d].level)?(e.from!=e.to&&(Wf=d),c):(e.from!=e.to&&(Wf=c),d);d=c}}return d}function Yf(a,b,c,d){if(!d)return b+c;do b+=c;while(b>0&&yf(a.text.charAt(b)));return b}function Zf(a,b,c,d){var e=Fe(a);if(!e)return $f(a,b,c,d);for(var f=Xf(e,b),g=e[f],h=Yf(a,b,g.level%2?-c:c,d);;){if(h>g.from&&h0==g.level%2?g.to:g.from);if(g=e[f+=c],!g)return null;h=c>0==g.level%2?Yf(a,g.to,-1,d):Yf(a,g.from,1,d)}}function $f(a,b,c,d){var e=b+c;if(d)for(;e>0&&yf(a.text.charAt(e));)e+=c;return 0>e||e>a.text.length?null:e}var a=/gecko\/\d/i.test(navigator.userAgent),b=/MSIE \d/.test(navigator.userAgent),c=b&&(null==document.documentMode||document.documentMode<8),d=b&&(null==document.documentMode||document.documentMode<9),e=b&&(null==document.documentMode||document.documentMode<10),f=/Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent),g=b||f,h=/WebKit\//.test(navigator.userAgent),i=h&&/Qt\/\d+\.\d+/.test(navigator.userAgent),j=/Chrome\//.test(navigator.userAgent),k=/Opera\//.test(navigator.userAgent),l=/Apple Computer/.test(navigator.vendor),m=/KHTML\//.test(navigator.userAgent),n=/Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent),o=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent),p=/PhantomJS/.test(navigator.userAgent),q=/AppleWebKit/.test(navigator.userAgent)&&/Mobile\/\w+/.test(navigator.userAgent),r=q||/Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent),s=q||/Mac/.test(navigator.platform),t=/win/i.test(navigator.platform),u=k&&navigator.userAgent.match(/Version\/(\d*\.\d*)/);u&&(u=Number(u[1])),u&&u>=15&&(k=!1,h=!0);var Cb,Wb,Xb,v=s&&(i||k&&(null==u||12.11>u)),w=a||g&&!d,x=!1,y=!1,Fb=0,ac=0,fc=0,gc=null;g?gc=-.53:a?gc=15:j?gc=-.7:l&&(gc=-1/3);var kc,tc,oc=null,vc=z.changeEnd=function(a){return a.text?Gc(a.from.line+a.text.length-1,nf(a.text).length+(1==a.text.length?a.from.ch:0)):a.to};z.Pos=Gc,z.prototype={constructor:z,focus:function(){window.focus(),Qb(this),Nb(this)},setOption:function(a,b){var c=this.options,d=c[a];(c[a]!=b||"mode"==a)&&(c[a]=b,ed.hasOwnProperty(a)&&Ib(this,ed[a])(this,b,d))},getOption:function(a){return this.options[a]},getDoc:function(){return this.doc},addKeyMap:function(a,b){this.state.keyMaps[b?"push":"unshift"](a)},removeKeyMap:function(a){for(var b=this.state.keyMaps,c=0;c=d;++d)$c(this,d,a)}),getTokenAt:function(a,b){var c=this.doc;a=Mc(c,a);for(var d=hb(this,a.line,b),e=this.doc.mode,f=ye(c,a.line),g=new vd(f.text,this.options.tabSize);g.pos>1;if((f?b[2*f-1]:0)>=e)d=f;else{if(!(b[2*f+1]d&&(a=d,c=!0);var e=ye(this.doc,a);return vb(this,ye(this.doc,a),{top:0,left:0},b||"page").top+(c?e.height:0)},defaultTextHeight:function(){return Db(this.display)},defaultCharWidth:function(){return Eb(this.display)},setGutterMarker:Ib(null,function(a,b,c){return _c(this,a,function(a){var d=a.gutterMarkers||(a.gutterMarkers={});return d[b]=c,!c&&wf(d)&&(a.gutterMarkers=null),!0})}),clearGutter:Ib(null,function(a){var b=this,c=b.doc,d=c.first;c.iter(function(c){c.gutterMarkers&&c.gutterMarkers[a]&&(c.gutterMarkers[a]=null,Lb(b,d,d+1),wf(c.gutterMarkers)&&(c.gutterMarkers=null)),++d})}),addLineClass:Ib(null,function(a,b,c){return _c(this,a,function(a){var d="text"==b?"textClass":"background"==b?"bgClass":"wrapClass";if(a[d]){if(new RegExp("(?:^|\\s)"+c+"(?:$|\\s)").test(a[d]))return!1;a[d]+=" "+c}else a[d]=c;return!0})}),removeLineClass:Ib(null,function(a,b,c){return _c(this,a,function(a){var d="text"==b?"textClass":"background"==b?"bgClass":"wrapClass",e=a[d];if(!e)return!1;if(null==c)a[d]=null;else{var f=e.match(new RegExp("(?:^|\\s+)"+c+"(?:$|\\s+)"));if(!f)return!1;var g=f.index+f[0].length;a[d]=e.slice(0,f.index)+(f.index&&g!=e.length?" ":"")+e.slice(g)||null}return!0})}),addLineWidget:Ib(null,function(a,b,c){return Zd(this,a,b,c)}),removeLineWidget:function(a){a.clear()},lineInfo:function(a){if("number"==typeof a){if(!Oc(this.doc,a))return null;var b=a;if(a=ye(this.doc,a),!a)return null}else{var b=Ce(a);if(null==b)return null}return{line:b,handle:a,text:a.text,gutterMarkers:a.gutterMarkers,textClass:a.textClass,bgClass:a.bgClass,wrapClass:a.wrapClass,widgets:a.widgets}},getViewport:function(){return{from:this.display.showingFrom,to:this.display.showingTo}},addWidget:function(a,b,c,d,e){var f=this.display;a=yb(this,Mc(this.doc,a));var g=a.bottom,h=a.left;if(b.style.position="absolute",f.sizer.appendChild(b),"over"==d)g=a.top;else if("above"==d||"near"==d){var i=Math.max(f.wrapper.clientHeight,this.doc.height),j=Math.max(f.sizer.clientWidth,f.lineSpace.clientWidth);("above"==d||a.bottom+b.offsetHeight>i)&&a.top>b.offsetHeight?g=a.top-b.offsetHeight:a.bottom+b.offsetHeight<=i&&(g=a.bottom),h+b.offsetWidth>j&&(h=j-b.offsetWidth)}b.style.top=g+"px",b.style.left=b.style.right="","right"==e?(h=f.sizer.clientWidth-b.offsetWidth,b.style.right="0px"):("left"==e?h=0:"middle"==e&&(h=(f.sizer.clientWidth-b.offsetWidth)/2),b.style.left=h+"px"),c&&Wc(this,h,g,h+b.offsetWidth,g+b.offsetHeight)},triggerOnKeyDown:Ib(null,pc),triggerOnKeyPress:Ib(null,qc),triggerOnKeyUp:Ib(null,nc),execCommand:function(a){return pd.hasOwnProperty(a)?pd[a](this):void 0},findPosH:function(a,b,c,d){var e=1;0>b&&(e=-1,b=-b);for(var f=0,g=Mc(this.doc,a);b>f&&(g=ad(this.doc,g,e,c,d),!g.hitSide);++f);return g},moveH:Ib(null,function(a,b){var d,c=this.doc.sel;d=c.shift||c.extend||Hc(c.from,c.to)?ad(this.doc,c.head,a,b,this.options.rtlMoveVisually):0>a?c.from:c.to,Pc(this.doc,d,d,a)}),deleteH:Ib(null,function(a,b){var c=this.doc.sel;Hc(c.from,c.to)?Fc(this.doc,"",c.from,ad(this.doc,c.head,a,b,!1),"+delete"):Fc(this.doc,"",c.from,c.to,"+delete"),this.curOp.userSelChange=!0}),findPosV:function(a,b,c,d){var e=1,f=d;0>b&&(e=-1,b=-b);for(var g=0,h=Mc(this.doc,a);b>g;++g){var i=yb(this,h,"div");if(null==f?f=i.left:i.left=f,h=bd(this,i,e,c),h.hitSide)break}return h},moveV:Ib(null,function(a,b){var d,e,c=this.doc.sel;if(c.shift||c.extend||Hc(c.from,c.to)){var f=yb(this,c.head,"div");null!=c.goalColumn&&(f.left=c.goalColumn),d=bd(this,f,a,b),"page"==b&&Zc(this,0,xb(this,d,"div").top-f.top),e=f.left}else d=0>a?c.from:c.to;Pc(this.doc,d,d,a),null!=e&&(c.goalColumn=e)}),toggleOverwrite:function(a){(null==a||a!=this.state.overwrite)&&((this.state.overwrite=!this.state.overwrite)?this.display.cursor.className+=" CodeMirror-overwrite":this.display.cursor.className=this.display.cursor.className.replace(" CodeMirror-overwrite",""),$e(this,"overwriteToggle",this,this.state.overwrite))},hasFocus:function(){return document.activeElement==this.display.input},scrollTo:Ib(null,function(a,b){Yc(this,a,b)}),getScrollInfo:function(){var a=this.display.scroller,b=gf;return{left:a.scrollLeft,top:a.scrollTop,height:a.scrollHeight-b,width:a.scrollWidth-b,clientHeight:a.clientHeight-b,clientWidth:a.clientWidth-b}},scrollIntoView:Ib(null,function(a,b){null==a?a={from:this.doc.sel.head,to:null}:"number"==typeof a?a={from:Gc(a,0),to:null}:null==a.from&&(a={from:a,to:null}),a.to||(a.to=a.from),b||(b=0);var c=a;null!=a.from.line&&(this.curOp.scrollToPos={from:a.from,to:a.to,margin:b},c={from:yb(this,a.from),to:yb(this,a.to)});var d=Xc(this,Math.min(c.from.left,c.to.left),Math.min(c.from.top,c.to.top)-b,Math.max(c.from.right,c.to.right),Math.max(c.from.bottom,c.to.bottom)+b);Yc(this,d.scrollLeft,d.scrollTop)}),setSize:Ib(null,function(a,b){function c(a){return"number"==typeof a||/^\d+$/.test(String(a))?a+"px":a}null!=a&&(this.display.wrapper.style.width=c(a)),null!=b&&(this.display.wrapper.style.height=c(b)),this.options.lineWrapping&&(this.display.measureLineCache.length=this.display.measureLineCachePos=0),this.curOp.forceUpdate=!0,$e(this,"refresh",this)}),operation:function(a){return Kb(this,a)},refresh:Ib(null,function(){var a=this.display.cachedTextHeight;sb(this),Yc(this,this.doc.scrollLeft,this.doc.scrollTop),Lb(this),(null==a||Math.abs(a-Db(this.display))>.5)&&F(this),$e(this,"refresh",this)}),swapDoc:Ib(null,function(a){var b=this.doc;return b.cm=null,xe(this,a),sb(this),Pb(this,!0),Yc(this,a.scrollLeft,a.scrollTop),bf(this,"swapDoc",this,b),b}),getInputField:function(){return this.display.input},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},ff(z);var ed=z.optionHandlers={},fd=z.defaults={},hd=z.Init={toString:function(){return"CodeMirror.Init"}};gd("value","",function(a,b){a.setValue(b)},!0),gd("mode",null,function(a,b){a.doc.modeOption=b,B(a)},!0),gd("indentUnit",2,B,!0),gd("indentWithTabs",!1),gd("smartIndent",!0),gd("tabSize",4,function(a){C(a),sb(a),Lb(a)},!0),gd("specialChars",/[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\ufeff]/g,function(a,b){a.options.specialChars=new RegExp(b.source+(b.test(" ")?"":"| "),"g"),a.refresh()},!0),gd("specialCharPlaceholder",je,function(a){a.refresh()},!0),gd("electricChars",!0),gd("rtlMoveVisually",!t),gd("wholeLineUpdateBefore",!0),gd("theme","default",function(a){H(a),I(a)},!0),gd("keyMap","default",G),gd("extraKeys",null),gd("onKeyEvent",null),gd("onDragEvent",null),gd("lineWrapping",!1,D,!0),gd("gutters",[],function(a){M(a.options),I(a)},!0),gd("fixedGutter",!0,function(a,b){a.display.gutters.style.left=b?S(a.display)+"px":"0",a.refresh()},!0),gd("coverGutterNextToScrollbar",!1,N,!0),gd("lineNumbers",!1,function(a){M(a.options),I(a)},!0),gd("firstLineNumber",1,I,!0),gd("lineNumberFormatter",function(a){return a},I,!0),gd("showCursorWhenSelecting",!1,ab,!0),gd("resetSelectionOnContextMenu",!0),gd("readOnly",!1,function(a,b){"nocursor"==b?(sc(a),a.display.input.blur(),a.display.disabled=!0):(a.display.disabled=!1,b||Pb(a,!0))}),gd("disableInput",!1,function(a,b){b||Pb(a,!0)},!0),gd("dragDrop",!0),gd("cursorBlinkRate",530),gd("cursorScrollMargin",0),gd("cursorHeight",1),gd("workTime",100),gd("workDelay",100),gd("flattenSpans",!0,C,!0),gd("addModeClass",!1,C,!0),gd("pollInterval",100),gd("undoDepth",40,function(a,b){a.doc.history.undoDepth=b}),gd("historyEventDelay",500),gd("viewportMargin",10,function(a){a.refresh()},!0),gd("maxHighlightLength",1e4,C,!0),gd("crudeMeasuringFrom",1e4),gd("moveInputWithCursor",!0,function(a,b){b||(a.display.inputDiv.style.top=a.display.inputDiv.style.left=0)}),gd("tabindex",null,function(a,b){a.display.input.tabIndex=b||""}),gd("autofocus",null);var id=z.modes={},jd=z.mimeModes={};z.defineMode=function(a,b){if(z.defaults.mode||"null"==a||(z.defaults.mode=a),arguments.length>2){b.dependencies=[];for(var c=2;c0&&b.ch=this.string.length},sol:function(){return this.pos==this.lineStart},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){return this.posb},eatSpace:function(){for(var a=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>a},skipToEnd:function(){this.pos=this.string.length},skipTo:function(a){var b=this.string.indexOf(a,this.pos);return b>-1?(this.pos=b,!0):void 0},backUp:function(a){this.pos-=a},column:function(){return this.lastColumnPos0?null:(f&&b!==!1&&(this.pos+=f[0].length),f)}var d=function(a){return c?a.toLowerCase():a},e=this.string.substr(this.pos,a.length);return d(e)==d(a)?(b!==!1&&(this.pos+=a.length),!0):void 0},current:function(){return this.string.slice(this.start,this.pos)},hideFirstChars:function(a,b){this.lineStart+=a;try{return b()}finally{this.lineStart-=a}}},z.StringStream=vd,z.TextMarker=wd,ff(wd),wd.prototype.clear=function(){if(!this.explicitlyCleared){var a=this.doc.cm,b=a&&!a.curOp;if(b&&Gb(a),ef(this,"clear")){var c=this.find();c&&bf(this,"clear",c.from,c.to)}for(var d=null,e=null,f=0;fa.display.maxLineLength&&(a.display.maxLine=i,a.display.maxLineLength=j,a.display.maxLineChanged=!0)}null!=d&&a&&Lb(a,d,e+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,a&&Sc(a)),bf(a,"markerCleared",a,this),b&&Hb(a)}},wd.prototype.find=function(a){for(var b,c,d=0;d=b.display.showingFrom&&a.linec;++c){var e=this.lines[c];this.height-=e.height,ae(e),bf(e,"delete")}this.lines.splice(a,b)},collapse:function(a){a.splice.apply(a,[a.length,0].concat(this.lines))},insertInner:function(a,b,c){this.height+=c,this.lines=this.lines.slice(0,a).concat(b).concat(this.lines.slice(a));for(var d=0,e=b.length;e>d;++d)b[d].parent=this},iterN:function(a,b,c){for(var d=a+b;d>a;++a)if(c(this.lines[a]))return!0}},re.prototype={chunkSize:function(){return this.size},removeInner:function(a,b){this.size-=b;for(var c=0;ca){var f=Math.min(b,e-a),g=d.height;if(d.removeInner(a,f),this.height-=g-d.height,e==f&&(this.children.splice(c--,1),d.parent=null),0==(b-=f))break;a=0}else a-=e}if(this.size-b<25){var h=[];this.collapse(h),this.children=[new qe(h)],this.children[0].parent=this}},collapse:function(a){for(var b=0,c=this.children.length;c>b;++b)this.children[b].collapse(a)},insertInner:function(a,b,c){this.size+=b.length,this.height+=c;for(var d=0,e=this.children.length;e>d;++d){var f=this.children[d],g=f.chunkSize();if(g>=a){if(f.insertInner(a,b,c),f.lines&&f.lines.length>50){for(;f.lines.length>50;){var h=f.lines.splice(f.lines.length-25,25),i=new qe(h);f.height-=i.height,this.children.splice(d+1,0,i),i.parent=this}this.maybeSpill()}break}a-=g}},maybeSpill:function(){if(!(this.children.length<=10)){var a=this;do{var b=a.children.splice(a.children.length-5,5),c=new re(b);if(a.parent){a.size-=c.size,a.height-=c.height;var e=pf(a.parent.children,a);a.parent.children.splice(e+1,0,c)}else{var d=new re(a.children);d.parent=a,a.children=[d,c],a=d}c.parent=a.parent}while(a.children.length>10);a.parent.maybeSpill()}},iterN:function(a,b,c){for(var d=0,e=this.children.length;e>d;++d){var f=this.children[d],g=f.chunkSize();if(g>a){var h=Math.min(b,g-a);if(f.iterN(a,h,c))return!0;if(0==(b-=h))break;a=0}else a-=g}}};var se=0,te=z.Doc=function(a,b,c){if(!(this instanceof te))return new te(a,b,c);null==c&&(c=0),re.call(this,[new qe([new $d("",null)])]),this.first=c,this.scrollTop=this.scrollLeft=0,this.cantEdit=!1,this.history=Ge(),this.cleanGeneration=1,this.frontier=c;var d=Gc(c,0);this.sel={from:d,to:d,head:d,anchor:d,shift:!1,extend:!1,goalColumn:null},this.id=++se,this.modeOption=b,"string"==typeof a&&(a=Kf(a)),pe(this,{from:d,to:d,text:a},null,{head:d,anchor:d})};te.prototype=qf(re.prototype,{constructor:te,iter:function(a,b,c){c?this.iterN(a-this.first,b-a,c):this.iterN(this.first,this.first+this.size,a)},insert:function(a,b){for(var c=0,d=0,e=b.length;e>d;++d)c+=b[d].height;this.insertInner(a-this.first,b,c)},remove:function(a,b){this.removeInner(a-this.first,b)},getValue:function(a){var b=Ae(this,this.first,this.first+this.size);return a===!1?b:b.join(a||"\n")},setValue:function(a){var b=Gc(this.first,0),c=this.first+this.size-1;zc(this,{from:b,to:Gc(c,ye(this,c).text.length),text:Kf(a),origin:"setValue"},{head:b,anchor:b},!0)},replaceRange:function(a,b,c,d){b=Mc(this,b),c=c?Mc(this,c):b,Fc(this,a,b,c,d)},getRange:function(a,b,c){var d=ze(this,Mc(this,a),Mc(this,b));return c===!1?d:d.join(c||"\n")},getLine:function(a){var b=this.getLineHandle(a);return b&&b.text},setLine:function(a,b){Oc(this,a)&&Fc(this,b,Gc(a,0),Mc(this,Gc(a)))},removeLine:function(a){a?Fc(this,"",Mc(this,Gc(a-1)),Mc(this,Gc(a))):Fc(this,"",Gc(0,0),Mc(this,Gc(1,0)))},getLineHandle:function(a){return Oc(this,a)?ye(this,a):void 0},getLineNumber:function(a){return Ce(a)},getLineHandleVisualStart:function(a){return"number"==typeof a&&(a=ye(this,a)),Rd(this,a)},lineCount:function(){return this.size},firstLine:function(){return this.first},lastLine:function(){return this.first+this.size-1},clipPos:function(a){return Mc(this,a)},getCursor:function(a){var c,b=this.sel;return c=null==a||"head"==a?b.head:"anchor"==a?b.anchor:"end"==a||a===!1?b.to:b.from,Kc(c)},somethingSelected:function(){return!Hc(this.sel.head,this.sel.anchor)},setCursor:Jb(function(a,b,c){var d=Mc(this,"number"==typeof a?Gc(a,b||0):a);c?Pc(this,d):Rc(this,d,d)}),setSelection:Jb(function(a,b,c){Rc(this,Mc(this,a),Mc(this,b||a),c)}),extendSelection:Jb(function(a,b,c){Pc(this,Mc(this,a),b&&Mc(this,b),c)}),getSelection:function(a){return this.getRange(this.sel.from,this.sel.to,a)},replaceSelection:function(a,b,c){zc(this,{from:this.sel.from,to:this.sel.to,text:Kf(a),origin:c},b||"around")},undo:Jb(function(){Bc(this,"undo")}),redo:Jb(function(){Bc(this,"redo")}),setExtending:function(a){this.sel.extend=a},historySize:function(){var a=this.history;return{undo:a.done.length,redo:a.undone.length}},clearHistory:function(){this.history=Ge(this.history.maxGeneration)},markClean:function(){this.cleanGeneration=this.changeGeneration(!0)},changeGeneration:function(a){return a&&(this.history.lastOp=this.history.lastOrigin=null),this.history.generation},isClean:function(a){return this.history.generation==(a||this.cleanGeneration)},getHistory:function(){return{done:Me(this.history.done),undone:Me(this.history.undone)}},setHistory:function(a){var b=this.history=Ge(this.history.maxGeneration);b.done=a.done.slice(0),b.undone=a.undone.slice(0)},markText:function(a,b,c){return yd(this,Mc(this,a),Mc(this,b),c,"range")},setBookmark:function(a,b){var c={replacedWith:b&&(null==b.nodeType?b.widget:b),insertLeft:b&&b.insertLeft,clearWhenEmpty:!1};return a=Mc(this,a),yd(this,a,a,c,"bookmark")},findMarksAt:function(a){a=Mc(this,a);var b=[],c=ye(this,a.line).markedSpans;if(c)for(var d=0;d=a.ch)&&b.push(e.marker.parent||e.marker)}return b},findMarks:function(a,b){a=Mc(this,a),b=Mc(this,b);var c=[],d=a.line;return this.iter(a.line,b.line+1,function(e){var f=e.markedSpans;if(f)for(var g=0;gh.to||null==h.from&&d!=a.line||d==b.line&&h.from>b.ch||c.push(h.marker.parent||h.marker)}++d}),c},getAllMarks:function(){var a=[];return this.iter(function(b){var c=b.markedSpans;if(c)for(var d=0;da?(b=a,!0):(a-=e,++c,void 0)}),Mc(this,Gc(c,b))},indexFromPos:function(a){a=Mc(this,a);var b=a.ch;return a.lineb&&(b=a.from),null!=a.to&&a.to=8208&&8212>=c}:h&&(Ff=function(a,b){if(b>1&&45==a.charCodeAt(b-1)){if(/\w/.test(a.charAt(b-2))&&/[^\-?\.]/.test(a.charAt(b)))return!0;if(b>2&&/[\d\.,]/.test(a.charAt(b-2))&&/[\d\.,]/.test(a.charAt(b)))return!1}return/[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|\u2026[\w~`@#$%\^&*(_=+{[><]/.test(a.slice(b-1,b+1))});var Gf,If,Kf=3!="\n\nb".split(/\n/).length?function(a){for(var b=0,c=[],d=a.length;d>=b;){var e=a.indexOf("\n",b);-1==e&&(e=a.length);var f=a.slice(b,"\r"==a.charAt(e-1)?e-1:e),g=f.indexOf("\r");-1!=g?(c.push(f.slice(0,g)),b+=g+1):(c.push(f),b=e+1)}return c}:function(a){return a.split(/\r\n?|\n/)};z.splitLines=Kf;var Lf=window.getSelection?function(a){try{return a.selectionStart!=a.selectionEnd}catch(b){return!1}}:function(a){try{var b=a.ownerDocument.selection.createRange()}catch(c){}return b&&b.parentElement()==a?0!=b.compareEndPoints("StartToEnd",b):!1},Mf=function(){var a=zf("div");return"oncopy"in a?!0:(a.setAttribute("oncopy","return;"),"function"==typeof a.oncopy)}(),Nf={3:"Enter",8:"Backspace",9:"Tab",13:"Enter",16:"Shift",17:"Ctrl",18:"Alt",19:"Pause",20:"CapsLock",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"PrintScrn",45:"Insert",46:"Delete",59:";",61:"=",91:"Mod",92:"Mod",93:"Mod",107:"=",109:"-",127:"Delete",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",63232:"Up",63233:"Down",63234:"Left",63235:"Right",63272:"Delete",63273:"Home",63275:"End",63276:"PageUp",63277:"PageDown",63302:"Insert"};z.keyNames=Nf,function(){for(var a=0;10>a;a++)Nf[a+48]=Nf[a+96]=String(a);for(var a=65;90>=a;a++)Nf[a]=String.fromCharCode(a);for(var a=1;12>=a;a++)Nf[a+111]=Nf[a+63235]="F"+a}();var Wf,_f=function(){function c(c){return 255>=c?a.charAt(c):c>=1424&&1524>=c?"R":c>=1536&&1791>=c?b.charAt(c-1536):c>=1792&&2220>=c?"r":"L"}var a="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLL",b="rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmmrrrrrrrrrrrrrrrrrr",d=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,e=/[stwN]/,f=/[LRr]/,g=/[Lb1n]/,h=/[1n]/,i="L";return function(a){if(!d.test(a))return!1;for(var l,b=a.length,j=[],k=0;b>k;++k)j.push(l=c(a.charCodeAt(k)));for(var k=0,m=i;b>k;++k){var l=j[k];"m"==l?j[k]=m:m=l}for(var k=0,n=i;b>k;++k){var l=j[k];"1"==l&&"r"==n?j[k]="n":f.test(l)&&(n=l,"r"==l&&(j[k]="R"))}for(var k=1,m=j[0];b-1>k;++k){var l=j[k];"+"==l&&"1"==m&&"1"==j[k+1]?j[k]="1":","!=l||m!=j[k+1]||"1"!=m&&"n"!=m||(j[k]=m),m=l}for(var k=0;b>k;++k){var l=j[k];if(","==l)j[k]="N";else if("%"==l){for(var o=k+1;b>o&&"%"==j[o];++o);for(var p=k&&"!"==j[k-1]||b>o&&"1"==j[o]?"1":"N",q=k;o>q;++q)j[q]=p;k=o-1}}for(var k=0,n=i;b>k;++k){var l=j[k];"L"==n&&"1"==l?j[k]="L":f.test(l)&&(n=l)}for(var k=0;b>k;++k)if(e.test(j[k])){for(var o=k+1;b>o&&e.test(j[o]);++o);for(var r="L"==(k?j[k-1]:i),s="L"==(b>o?j[o]:i),p=r||s?"L":"R",q=k;o>q;++q)j[q]=p;k=o-1}for(var u,t=[],k=0;b>k;)if(g.test(j[k])){var v=k;for(++k;b>k&&g.test(j[k]);++k);t.push({from:v,to:k,level:0})}else{var w=k,x=t.length;for(++k;b>k&&"L"!=j[k];++k);for(var q=w;k>q;)if(h.test(j[q])){q>w&&t.splice(x,0,{from:w,to:q,level:1});var y=q;for(++q;k>q&&h.test(j[q]);++q);t.splice(x,0,{from:y,to:q,level:2}),w=q}else++q;k>w&&t.splice(x,0,{from:w,to:k,level:1})}return 1==t[0].level&&(u=a.match(/^\s+/))&&(t[0].from=u[0].length,t.unshift({from:0,to:u[0].length,level:0})),1==nf(t).level&&(u=a.match(/\s+$/))&&(nf(t).to-=u[0].length,t.push({from:b-u[0].length,to:b,level:0})),t[0].level!=nf(t).level&&t.push({from:b,to:b,level:t[0].level}),t}}();return z.version="3.22.1",z}(),CodeMirror.defineMode("javascript",function(a,b){function k(a){for(var c,b=!1,d=!1;null!=(c=a.next());){if(!b){if("/"==c&&!d)return;"["==c?d=!0:d&&"]"==c&&(d=!1)}b=!b&&"\\"==c}}function n(a,b,c){return l=a,m=c,b}function o(a,b){var c=a.next();if('"'==c||"'"==c)return b.tokenize=p(c),b.tokenize(a,b);if("."==c&&a.match(/^\d+(?:[eE][+\-]?\d+)?/))return n("number","number");if("."==c&&a.match(".."))return n("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(c))return n(c);if("="==c&&a.eat(">"))return n("=>","operator");if("0"==c&&a.eat(/x/i))return a.eatWhile(/[\da-f]/i),n("number","number");if(/\d/.test(c))return a.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/),n("number","number");if("/"==c)return a.eat("*")?(b.tokenize=q,q(a,b)):a.eat("/")?(a.skipToEnd(),n("comment","comment")):"operator"==b.lastType||"keyword c"==b.lastType||"sof"==b.lastType||/^[\[{}\(,;:]$/.test(b.lastType)?(k(a),a.eatWhile(/[gimy]/),n("regexp","string-2")):(a.eatWhile(i),n("operator","operator",a.current()));if("`"==c)return b.tokenize=r,r(a,b);if("#"==c)return a.skipToEnd(),n("error","error");if(i.test(c))return a.eatWhile(i),n("operator","operator",a.current());a.eatWhile(/[\w\$_]/);var d=a.current(),e=h.propertyIsEnumerable(d)&&h[d];return e&&"."!=b.lastType?n(e.type,e.style,d):n("variable","variable",d)}function p(a){return function(b,c){var f,d=!1;if(e&&"@"==b.peek()&&b.match(j))return c.tokenize=o,n("jsonld-keyword","meta");for(;null!=(f=b.next())&&(f!=a||d);)d=!d&&"\\"==f;return d||(c.tokenize=o),n("string","string")}}function q(a,b){for(var d,c=!1;d=a.next();){if("/"==d&&c){b.tokenize=o;break}c="*"==d}return n("comment","comment")}function r(a,b){for(var d,c=!1;null!=(d=a.next());){if(!c&&("`"==d||"$"==d&&a.eat("{"))){b.tokenize=o;break}c=!c&&"\\"==d}return n("quasi","string-2",a.current())}function t(a,b){b.fatArrowAt&&(b.fatArrowAt=null);var c=a.string.indexOf("=>",a.start);if(!(0>c)){for(var d=0,e=!1,f=c-1;f>=0;--f){var g=a.string.charAt(f),h=s.indexOf(g);if(h>=0&&3>h){if(!d){++f;break}if(0==--d)break}else if(h>=3&&6>h)++d;else if(/[$\w]/.test(g))e=!0;else if(e&&!d){++f;break}}e&&!d&&(b.fatArrowAt=f)}}function v(a,b,c,d,e,f){this.indented=a,this.column=b,this.type=c,this.prev=e,this.info=f,null!=d&&(this.align=d)}function w(a,b){for(var c=a.localVars;c;c=c.next)if(c.name==b)return!0;for(var d=a.context;d;d=d.prev)for(var c=d.vars;c;c=c.next)if(c.name==b)return!0}function x(a,b,c,d,e){var g=a.cc;for(y.state=a,y.stream=e,y.marked=null,y.cc=g,a.lexical.hasOwnProperty("align")||(a.lexical.align=!0);;){var h=g.length?g.pop():f?J:I;if(h(c,d)){for(;g.length&&g[g.length-1].lex;)g.pop()();return y.marked?y.marked:"variable"==c&&w(a,d)?"variable-2":b}}}function z(){for(var a=arguments.length-1;a>=0;a--)y.cc.push(arguments[a])}function A(){return z.apply(null,arguments),!0}function B(a){function c(b){for(var c=b;c;c=c.next)if(c.name==a)return!0;return!1}var d=y.state;if(d.context){if(y.marked="def",c(d.localVars))return;d.localVars={name:a,next:d.localVars}}else{if(c(d.globalVars))return;b.globalVars&&(d.globalVars={name:a,next:d.globalVars})}}function D(){y.state.context={prev:y.state.context,vars:y.state.localVars},y.state.localVars=C}function E(){y.state.localVars=y.state.context.vars,y.state.context=y.state.context.prev}function F(a,b){var c=function(){var c=y.state,d=c.indented;"stat"==c.lexical.type&&(d=c.lexical.indented),c.lexical=new v(d,y.stream.column(),a,null,c.lexical,b)};return c.lex=!0,c}function G(){var a=y.state;a.lexical.prev&&(")"==a.lexical.type&&(a.indented=a.lexical.indented),a.lexical=a.lexical.prev)}function H(a){return function(b){return b==a?A():";"==a?z():A(arguments.callee)}}function I(a,b){return"var"==a?A(F("vardef",b.length),cb,H(";"),G):"keyword a"==a?A(F("form"),J,I,G):"keyword b"==a?A(F("form"),I,G):"{"==a?A(F("}"),_,G):";"==a?A():"if"==a?A(F("form"),J,I,G,hb):"function"==a?A(nb):"for"==a?A(F("form"),ib,I,G):"variable"==a?A(F("stat"),U):"switch"==a?A(F("form"),J,F("}","switch"),H("{"),_,G,G):"case"==a?A(J,H(":")):"default"==a?A(H(":")):"catch"==a?A(F("form"),D,H("("),ob,H(")"),I,G,E):"module"==a?A(F("form"),D,sb,E,G):"class"==a?A(F("form"),pb,rb,G):"export"==a?A(F("form"),tb,G):"import"==a?A(F("form"),ub,G):z(F("stat"),J,H(";"),G)}function J(a){return L(a,!1)}function K(a){return L(a,!0)}function L(a,b){if(y.state.fatArrowAt==y.stream.start){var c=b?T:S;if("("==a)return A(D,F(")"),Z(db,")"),G,H("=>"),c,E);if("variable"==a)return z(D,db,H("=>"),c,E)}var d=b?P:O;return u.hasOwnProperty(a)?A(d):"function"==a?A(nb):"keyword c"==a?A(b?N:M):"("==a?A(F(")"),M,zb,H(")"),G,d):"operator"==a||"spread"==a?A(b?K:J):"["==a?A(F("]"),xb,G,d):"{"==a?$(W,"}",null,d):A()}function M(a){return a.match(/[;\}\)\],]/)?z():z(J)}function N(a){return a.match(/[;\}\)\],]/)?z():z(K)}function O(a,b){return","==a?A(J):P(a,b,!1)}function P(a,b,c){var d=0==c?O:P,e=0==c?J:K;return"=>"==b?A(D,c?T:S,E):"operator"==a?/\+\+|--/.test(b)?A(d):"?"==b?A(J,H(":"),e):A(e):"quasi"==a?(y.cc.push(d),Q(b)):";"!=a?"("==a?$(K,")","call",d):"."==a?A(V,d):"["==a?A(F("]"),M,H("]"),G,d):void 0:void 0}function Q(a){return"${"!=a.slice(a.length-2)?A():A(J,R)}function R(a){return"}"==a?(y.marked="string-2",y.state.tokenize=r,A()):void 0}function S(a){return t(y.stream,y.state),"{"==a?z(I):z(J)}function T(a){return t(y.stream,y.state),"{"==a?z(I):z(K)}function U(a){return":"==a?A(G,I):z(O,H(";"),G)}function V(a){return"variable"==a?(y.marked="property",A()):void 0}function W(a,b){if("variable"==a){if(y.marked="property","get"==b||"set"==b)return A(X)}else if("number"==a||"string"==a)y.marked=e?"property":a+" property";else if("["==a)return A(J,H("]"),Y);return u.hasOwnProperty(a)?A(Y):void 0}function X(a){return"variable"!=a?z(Y):(y.marked="property",A(nb))}function Y(a){return":"==a?A(K):"("==a?z(nb):void 0}function Z(a,b){function c(d){if(","==d){var e=y.state.lexical;return"call"==e.info&&(e.pos=(e.pos||0)+1),A(a,c)}return d==b?A():A(H(b))}return function(d){return d==b?A():z(a,c)}}function $(a,b,c){for(var d=3;d!?|~^]/,j=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/,s="([{}])",u={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,"this":!0,"jsonld-keyword":!0},y={state:null,column:null,marked:null,cc:null},C={name:"this",next:{name:"arguments"}};return G.lex=!0,{startState:function(a){var d={tokenize:o,lastType:"sof",cc:[],lexical:new v((a||0)-c,0,"block",!1),localVars:b.localVars,context:b.localVars&&{vars:b.localVars},indented:0};return b.globalVars&&(d.globalVars=b.globalVars),d},token:function(a,b){if(a.sol()&&(b.lexical.hasOwnProperty("align")||(b.lexical.align=!1),b.indented=a.indentation(),t(a,b)),b.tokenize!=q&&a.eatSpace())return null;var c=b.tokenize(a,b);return"comment"==l?c:(b.lastType="operator"!=l||"++"!=m&&"--"!=m?l:"incdec",x(b,c,l,m,a))},indent:function(a,e){if(a.tokenize==q)return CodeMirror.Pass;if(a.tokenize!=o)return 0;for(var f=e&&e.charAt(0),g=a.lexical,h=a.cc.length-1;h>=0;--h){var i=a.cc[h];if(i==G)g=g.prev;else if(i!=hb)break}"stat"==g.type&&"}"==f&&(g=g.prev),d&&")"==g.type&&"stat"==g.prev.type&&(g=g.prev);var j=g.type,k=f==j;return"vardef"==j?g.indented+("operator"==a.lastType||","==a.lastType?g.info+1:0):"form"==j&&"{"==f?g.indented:"form"==j?g.indented+c:"stat"==j?g.indented+("operator"==a.lastType||","==a.lastType?d||c:0):"switch"!=g.info||k||0==b.doubleIndentSwitch?g.align?g.column+(k?0:1):g.indented+(k?0:c):g.indented+(/^(?:case|default)\b/.test(e)?c:2*c)},electricChars:":{}",blockCommentStart:f?null:"/*",blockCommentEnd:f?null:"*/",lineComment:f?null:"//",fold:"brace",helperType:f?"json":"javascript",jsonldMode:e,jsonMode:f}}),CodeMirror.defineMIME("text/javascript","javascript"),CodeMirror.defineMIME("text/ecmascript","javascript"),CodeMirror.defineMIME("application/javascript","javascript"),CodeMirror.defineMIME("application/ecmascript","javascript"),CodeMirror.defineMIME("application/json",{name:"javascript",json:!0}),CodeMirror.defineMIME("application/x-json",{name:"javascript",json:!0}),CodeMirror.defineMIME("application/ld+json",{name:"javascript",jsonld:!0}),CodeMirror.defineMIME("text/typescript",{name:"javascript",typescript:!0}),CodeMirror.defineMIME("application/typescript",{name:"javascript",typescript:!0}),function(){function d(a,d,e){function r(d,e,f){if(d.text){var h=m?0:d.text.length-1,i=m?d.text.length:-1;if(d.text.length>g)return null;for(null!=f&&(h=f+n);h!=i;h+=n){var j=d.text.charAt(h);if(q.test(j)&&a.getTokenTypeAt(b(e,h+1))==o){var k=c[j];if(">"==k.charAt(1)==m)p.push(j);else{if(p.pop()!=k.charAt(0))return{pos:h,match:!1};if(!p.length)return{pos:h,match:!0}}}}}}var f=a.state.matchBrackets,g=f&&f.maxScanLineLength||1e4,h=f&&f.maxScanLines||100,i=d||a.getCursor(),j=a.getLineHandle(i.line),k=i.ch-1,l=k>=0&&c[j.text.charAt(k)]||c[j.text.charAt(++k)];if(!l)return null;var m=">"==l.charAt(1),n=m?1:-1;if(e&&m!=(k==i.ch))return null;for(var t,o=a.getTokenTypeAt(b(i.line,k+1)),p=[j.text.charAt(k)],q=/[(){}[\]]/,s=i.line,u=m?Math.min(s+h,a.lineCount()):Math.max(-1,s-h);s!=u&&!(t=s==i.line?r(j,s,k):r(a.getLineHandle(s),s));s+=n);return{from:b(i.line,k),to:t&&b(s,t.pos),match:t&&t.match,forward:m}}function e(c,e){var f=c.state.matchBrackets.maxHighlightLineLength||1e3,g=d(c);if(!(!g||c.getLine(g.from.line).length>f||g.to&&c.getLine(g.to.line).length>f)){var h=g.match?"CodeMirror-matchingbracket":"CodeMirror-nonmatchingbracket",i=c.markText(g.from,b(g.from.line,g.from.ch+1),{className:h}),j=g.to&&c.markText(g.to,b(g.to.line,g.to.ch+1),{className:h});a&&c.state.focused&&c.display.input.focus();var k=function(){c.operation(function(){i.clear(),j&&j.clear()})};return e?(setTimeout(k,800),void 0):k +}}function g(a){a.operation(function(){f&&(f(),f=null),a.somethingSelected()||(f=e(a,!1))})}var a=/MSIE \d/.test(navigator.userAgent)&&(null==document.documentMode||document.documentMode<8),b=CodeMirror.Pos,c={"(":")>",")":"(<","[":"]>","]":"[<","{":"}>","}":"{<"},f=null;CodeMirror.defineOption("matchBrackets",!1,function(a,b,c){c&&c!=CodeMirror.Init&&a.off("cursorActivity",g),b&&(a.state.matchBrackets="object"==typeof b?b:{},a.on("cursorActivity",g))}),CodeMirror.defineExtension("matchBrackets",function(){e(this,!0)}),CodeMirror.defineExtension("findMatchingBracket",function(a,b){return d(this,a,b)})}(),CodeMirror.runMode=function(a,b,c,d){var e=CodeMirror.getMode(CodeMirror.defaults,b),f=/MSIE \d/.test(navigator.userAgent),g=f&&(null==document.documentMode||document.documentMode<9);if(1==c.nodeType){var h=d&&d.tabSize||CodeMirror.defaults.tabSize,i=c,j=0;i.innerHTML="",c=function(a,b){if("\n"==a)return i.appendChild(document.createTextNode(g?"\r":a)),j=0,void 0;for(var c="",d=0;;){var e=a.indexOf(" ",d);if(-1==e){c+=a.slice(d),j+=a.length-d;break}j+=e-d,c+=a.slice(d,e);var f=h-j%h;j+=f;for(var k=0;f>k;++k)c+=" ";d=e+1}if(b){var l=i.appendChild(document.createElement("span"));l.className="cm-"+b.replace(/ +/g," cm-"),l.appendChild(document.createTextNode(c))}else i.appendChild(document.createTextNode(c))}}for(var k=CodeMirror.splitLines(a),l=d&&d.state||CodeMirror.startState(e),m=0,n=k.length;n>m;++m){m&&c("\n");for(var o=new CodeMirror.StringStream(k[m]);!o.eol();){var p=e.token(o,l);c(o.current(),p,m,o.start,l),o.start=o.pos}}}; \ No newline at end of file diff --git a/addon-sdk/source/examples/actor-repl/data/codemirror.css b/addon-sdk/source/examples/actor-repl/data/codemirror.css new file mode 100644 index 000000000..2b050e19f --- /dev/null +++ b/addon-sdk/source/examples/actor-repl/data/codemirror.css @@ -0,0 +1,264 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; +} +.CodeMirror-scroll { + /* Set scrolling behaviour here */ + overflow: auto; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +/* CURSOR */ + +.CodeMirror div.CodeMirror-cursor { + border-left: 1px solid black; + z-index: 3; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; + z-index: 1; +} +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {} + +.cm-tab { display: inline-block; } + +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable {color: black;} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-property {color: black;} +.cm-s-default .cm-operator {color: black;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + line-height: 1; + position: relative; + overflow: hidden; + background: white; + color: black; +} + +.CodeMirror-scroll { + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + padding-bottom: 30px; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + -moz-box-sizing: content-box; + box-sizing: content-box; + padding-bottom: 30px; + margin-bottom: -32px; + display: inline-block; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} + +.CodeMirror-lines { + cursor: text; +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget {} + +.CodeMirror-wrap .CodeMirror-scroll { + overflow-x: hidden; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} +.CodeMirror-measure pre { position: static; } + +.CodeMirror div.CodeMirror-cursor { + position: absolute; + visibility: hidden; + border-right: none; + width: 0; +} +.CodeMirror-focused div.CodeMirror-cursor { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } + +.cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursor { + visibility: hidden; + } +} diff --git a/addon-sdk/source/examples/actor-repl/data/index.html b/addon-sdk/source/examples/actor-repl/data/index.html new file mode 100644 index 000000000..250ece249 --- /dev/null +++ b/addon-sdk/source/examples/actor-repl/data/index.html @@ -0,0 +1,147 @@ + + + + + + + +
+

+            
+
+
+ + +

+  
+  
+  
+
diff --git a/addon-sdk/source/examples/actor-repl/data/main.css b/addon-sdk/source/examples/actor-repl/data/main.css
new file mode 100644
index 000000000..03499944b
--- /dev/null
+++ b/addon-sdk/source/examples/actor-repl/data/main.css
@@ -0,0 +1,117 @@
+/* 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/. */
+
+body
+{
+  position: absolute;
+  width: 100%;
+  margin: 0;
+  padding: 0;
+  background: white;
+}
+
+
+pre
+{
+  margin: 0;
+}
+
+section
+{
+  border-top: 1px solid rgba(150, 150, 150, 0.5);
+}
+
+.CodeMirror {
+  height: auto;
+}
+.CodeMirror-scroll {
+  overflow-y: hidden;
+  overflow-x: auto;
+}
+
+.request,
+.response,
+.input
+{
+  border-left: 5px solid;
+  padding-left: 10px;
+}
+
+.request:not(:empty),
+.response.pending
+{
+  padding: 5px;
+}
+
+.input
+{
+  padding-left: 6px;
+  border-color: lightgreen;
+}
+.input.invalid
+{
+  border-color: orange;
+}
+
+.request
+{
+  border-color: lightgrey;
+}
+
+.response
+{
+  border-color: grey;
+}
+.response.error
+{
+  border-color: red;
+}
+
+.response.message
+{
+    border-color: lightblue;
+}
+
+.response .one,
+.response .two,
+.response .three
+{
+  width: 0;
+  height: auto;
+}
+
+
+
+.response.pending .one,
+.response.pending .two,
+.response.pending .three
+{
+  width: 10px;
+  height: 10px;
+  background-color: rgba(150, 150, 150, 0.5);
+
+  border-radius: 100%;
+  display: inline-block;
+  animation: bouncedelay 1.4s infinite ease-in-out;
+  /* Prevent first frame from flickering when animation starts */
+  animation-fill-mode: both;
+}
+
+.response.pending .one
+{
+  animation-delay: -0.32s;
+}
+
+.response.pending .two
+{
+  animation-delay: -0.16s;
+}
+
+@keyframes bouncedelay {
+  0%, 80%, 100% {
+    transform: scale(0.0);
+  } 40% {
+    transform: scale(1.0);
+  }
+}
diff --git a/addon-sdk/source/examples/actor-repl/data/robot.png b/addon-sdk/source/examples/actor-repl/data/robot.png
new file mode 100644
index 000000000..983516f98
Binary files /dev/null and b/addon-sdk/source/examples/actor-repl/data/robot.png differ
diff --git a/addon-sdk/source/examples/actor-repl/index.js b/addon-sdk/source/examples/actor-repl/index.js
new file mode 100644
index 000000000..b99aaf8a2
--- /dev/null
+++ b/addon-sdk/source/examples/actor-repl/index.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";
+
+const { Panel } = require("dev/panel");
+const { Tool } = require("dev/toolbox");
+const { Class } = require("sdk/core/heritage");
+
+
+const REPLPanel = Class({
+  extends: Panel,
+  label: "Actor REPL",
+  tooltip: "Firefox debugging protocol REPL",
+  icon: "./robot.png",
+  url: "./index.html",
+  setup: function({debuggee}) {
+    this.debuggee = debuggee;
+  },
+  dispose: function() {
+    this.debuggee = null;
+  },
+  onReady: function() {
+    console.log("repl panel document is interactive");
+    this.debuggee.start();
+    this.postMessage("RDP", [this.debuggee]);
+  },
+  onLoad: function() {
+    console.log("repl panel document is fully loaded");
+  }
+});
+exports.REPLPanel = REPLPanel;
+
+
+const replTool = new Tool({
+  panels: { repl: REPLPanel }
+});
diff --git a/addon-sdk/source/examples/actor-repl/package.json b/addon-sdk/source/examples/actor-repl/package.json
new file mode 100644
index 000000000..9dc1347e8
--- /dev/null
+++ b/addon-sdk/source/examples/actor-repl/package.json
@@ -0,0 +1,10 @@
+{
+  "name": "actor-repl",
+  "id": "@actor-repl",
+  "title": "Actor REPL",
+  "description": "Actor REPL",
+  "version": "0.0.1",
+  "author": "Irakli Gozalishvili",
+  "main": "./index.js",
+  "license": "MPL-2.0"
+}
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 000000000..9862fc20b
--- /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/debug-client/data/client.js b/addon-sdk/source/examples/debug-client/data/client.js
new file mode 100644
index 000000000..022f9a1c3
--- /dev/null
+++ b/addon-sdk/source/examples/debug-client/data/client.js
@@ -0,0 +1,816 @@
+/* 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(exports) {
+"use strict";
+
+
+var describe = Object.getOwnPropertyDescriptor;
+var Class = fields => {
+  var constructor = fields.constructor || function() {};
+  var ancestor = fields.extends || Object;
+
+
+
+  var descriptor = {};
+  for (var key of Object.keys(fields))
+    descriptor[key] = describe(fields, key);
+
+  var prototype = Object.create(ancestor.prototype, descriptor);
+
+  constructor.prototype = prototype;
+  prototype.constructor = constructor;
+
+  return constructor;
+};
+
+
+var bus = function Bus() {
+  var parser = new DOMParser();
+  return parser.parseFromString("", "application/xml").documentElement;
+}();
+
+var GUID = new WeakMap();
+GUID.id = 0;
+var guid = x => GUID.get(x);
+var setGUID = x => {
+  GUID.set(x, ++ GUID.id);
+};
+
+var Emitter = Class({
+  extends: EventTarget,
+  constructor: function() {
+   this.setupEmitter();
+  },
+  setupEmitter: function() {
+    setGUID(this);
+  },
+  addEventListener: function(type, listener, capture) {
+    bus.addEventListener(type + "@" + guid(this),
+                         listener, capture);
+  },
+  removeEventListener: function(type, listener, capture) {
+    bus.removeEventListener(type + "@" + guid(this),
+                            listener, capture);
+  }
+});
+
+function dispatch(target, type, data) {
+  var event = new MessageEvent(type + "@" + guid(target), {
+    bubbles: true,
+    cancelable: false,
+    data: data
+  });
+  bus.dispatchEvent(event);
+}
+
+var supervisedWorkers = new WeakMap();
+var supervised = supervisor => {
+  if (!supervisedWorkers.has(supervisor)) {
+    supervisedWorkers.set(supervisor, new Map());
+    supervisor.connection.addActorPool(supervisor);
+  }
+  return supervisedWorkers.get(supervisor);
+};
+
+var Supervisor = Class({
+  extends: Emitter,
+  constructor: function(...params) {
+    this.setupEmitter(...params);
+    this.setupSupervisor(...params);
+  },
+  Supervisor: function(connection) {
+    this.connection = connection;
+  },
+  /**
+   * Return the parent pool for this client.
+   */
+  supervisor: function() {
+    return this.connection.poolFor(this.actorID);
+  },
+  /**
+   * Override this if you want actors returned by this actor
+   * to belong to a different actor by default.
+   */
+  marshallPool: function() { return this; },
+    /**
+   * Add an actor as a child of this pool.
+   */
+  supervise: function(actor) {
+    if (!actor.actorID)
+      actor.actorID = this.connection.allocID(actor.actorPrefix ||
+                                              actor.typeName);
+
+    supervised(this).set(actor.actorID, actor);
+    return actor;
+  },
+  /**
+   * Remove an actor as a child of this pool.
+   */
+  abandon: function(actor) {
+    supervised(this).delete(actor.actorID);
+  },
+  // true if the given actor ID exists in the pool.
+  has: function(actorID) {
+    return supervised(this).has(actorID);
+  },
+  // Same as actor, should update debugger connection to use 'actor'
+  // and then remove this.
+  get: function(actorID) {
+    return supervised(this).get(actorID);
+  },
+  actor: function(actorID) {
+    return supervised(this).get(actorID);
+  },
+  isEmpty: function() {
+    return supervised(this).size === 0;
+  },
+  /**
+   * For getting along with the debugger server pools, should be removable
+   * eventually.
+   */
+  cleanup: function() {
+    this.destroy();
+  },
+  destroy: function() {
+    var supervisor = this.supervisor();
+    if (supervisor)
+      supervisor.abandon(this);
+
+    for (var actor of supervised(this).values()) {
+      if (actor !== this) {
+        var destroy = actor.destroy;
+        // Disconnect destroy while we're destroying in case of (misbehaving)
+        // circular ownership.
+        if (destroy) {
+          actor.destroy = null;
+          destroy.call(actor);
+          actor.destroy = destroy;
+        }
+      }
+    }
+
+    this.connection.removeActorPool(this);
+    supervised(this).clear();
+  }
+
+});
+
+
+
+
+var mailbox = new WeakMap();
+var clientRequests = new WeakMap();
+
+var inbox = client => mailbox.get(client).inbox;
+var outbox = client => mailbox.get(client).outbox;
+var requests = client => clientRequests.get(client);
+
+
+var Receiver = Class({
+  receive: function(packet) {
+    if (packet.error)
+      this.reject(packet.error);
+    else
+      this.resolve(this.read(packet));
+  }
+});
+
+var Connection = Class({
+  constructor: function() {
+    // Queue of the outgoing messages.
+    this.outbox = [];
+    // Map of pending requests.
+    this.pending = new Map();
+    this.pools = new Set();
+  },
+  isConnected: function() {
+    return !!this.port
+  },
+  connect: function(port) {
+    this.port = port;
+    port.addEventListener("message", this);
+    port.start();
+
+    this.flush();
+  },
+  addPool: function(pool) {
+    this.pools.add(pool);
+  },
+  removePool: function(pool) {
+    this.pools.delete(pool);
+  },
+  poolFor: function(id) {
+    for (let pool of this.pools.values()) {
+      if (pool.has(id))
+        return pool;
+    }
+  },
+  get: function(id) {
+    var pool = this.poolFor(id);
+    return pool && pool.get(id);
+  },
+  disconnect: function() {
+    this.port.stop();
+    this.port = null;
+    for (var request of this.pending.values()) {
+      request.catch(new Error("Connection closed"));
+    }
+    this.pending.clear();
+
+    var requests = this.outbox.splice(0);
+    for (var request of request) {
+      requests.catch(new Error("Connection closed"));
+    }
+  },
+  handleEvent: function(event) {
+    this.receive(event.data);
+  },
+  flush: function() {
+    if (this.isConnected()) {
+      for (var request of this.outbox) {
+        if (!this.pending.has(request.to)) {
+          this.outbox.splice(this.outbox.indexOf(request), 1);
+          this.pending.set(request.to, request);
+          this.send(request.packet);
+        }
+      }
+    }
+  },
+  send: function(packet) {
+    this.port.postMessage(packet);
+  },
+  request: function(packet) {
+    return new Promise(function(resolve, reject) {
+      this.outbox.push({
+        to: packet.to,
+        packet: packet,
+        receive: resolve,
+        catch: reject
+      });
+      this.flush();
+    });
+  },
+  receive: function(packet) {
+    var { from, type, why } = packet;
+    var receiver = this.pending.get(from);
+    if (!receiver) {
+      console.warn("Unable to handle received packet", data);
+    } else {
+      this.pending.delete(from);
+      if (packet.error)
+        receiver.catch(packet.error);
+      else
+        receiver.receive(packet);
+    }
+    this.flush();
+  },
+});
+
+/**
+ * Base class for client-side actor fronts.
+ */
+var Client = Class({
+  extends: Supervisor,
+  constructor: function(from=null, detail=null, connection=null) {
+    this.Client(from, detail, connection);
+  },
+  Client: function(form, detail, connection) {
+    this.Supervisor(connection);
+
+    if (form) {
+      this.actorID = form.actor;
+      this.from(form, detail);
+    }
+  },
+  connect: function(port) {
+    this.connection = new Connection(port);
+  },
+  actorID: null,
+  actor: function() {
+    return this.actorID;
+  },
+  /**
+   * Update the actor from its representation.
+   * Subclasses should override this.
+   */
+  form: function(form) {
+  },
+  /**
+   * Method is invokeid when packet received constitutes an
+   * event. By default such packets are demarshalled and
+   * dispatched on the client instance.
+   */
+  dispatch: function(packet) {
+  },
+  /**
+   * Method is invoked when packet is returned in response to
+   * a request. By default respond delivers response to a first
+   * request in a queue.
+   */
+  read: function(input) {
+    throw new TypeError("Subclass must implement read method");
+  },
+  write: function(input) {
+    throw new TypeError("Subclass must implement write method");
+  },
+  respond: function(packet) {
+    var [resolve, reject] = requests(this).shift();
+    if (packet.error)
+      reject(packet.error);
+    else
+      resolve(this.read(packet));
+  },
+  receive: function(packet) {
+    if (this.isEventPacket(packet)) {
+      this.dispatch(packet);
+    }
+    else if (requests(this).length) {
+      this.respond(packet);
+    }
+    else {
+      this.catch(packet);
+    }
+  },
+  send: function(packet) {
+    Promise.cast(packet.to || this.actor()).then(id => {
+      packet.to = id;
+      this.connection.send(packet);
+    })
+  },
+  request: function(packet) {
+    return this.connection.request(packet);
+  }
+});
+
+
+var Destructor = method => {
+  return function(...args) {
+    return method.apply(this, args).then(result => {
+      this.destroy();
+      return result;
+    });
+  };
+};
+
+var Profiled = (method, id) => {
+  return function(...args) {
+    var start = new Date();
+    return method.apply(this, args).then(result => {
+      var end = new Date();
+      this.telemetry.add(id, +end - start);
+      return result;
+    });
+  };
+};
+
+var Method = (request, response) => {
+  return response ? new BidirectionalMethod(request, response) :
+         new UnidirecationalMethod(request);
+};
+
+var UnidirecationalMethod = request => {
+  return function(...args) {
+    var packet = request.write(args, this);
+    this.connection.send(packet);
+    return Promise.resolve(void(0));
+  };
+};
+
+var BidirectionalMethod = (request, response) => {
+  return function(...args) {
+    var packet = request.write(args, this);
+    return this.connection.request(packet).then(packet => {
+      return response.read(packet, this);
+    });
+  };
+};
+
+
+Client.from = ({category, typeName, methods, events}) => {
+  var proto = {
+    constructor: function(...args) {
+      this.Client(...args);
+    },
+    extends: Client,
+    name: typeName
+  };
+
+  methods.forEach(({telemetry, request, response, name, oneway, release}) => {
+    var [reader, writer] = oneway ? [, new Request(request)] :
+                           [new Request(request), new Response(response)];
+    var method = new Method(request, response);
+    var profiler = telemetry ? new Profiler(method) : method;
+    var destructor = release ? new Destructor(profiler) : profiler;
+    proto[name] = destructor;
+  });
+
+  return Class(proto);
+};
+
+
+var defineType = (client, descriptor) => {
+  var type = void(0)
+  if (typeof(descriptor) === "string") {
+    if (name.indexOf(":") > 0)
+      type = makeCompoundType(descriptor);
+    else if (name.indexOf("#") > 0)
+      type = new ActorDetail(descriptor);
+    else if (client.specification[descriptor])
+      type = makeCategoryType(client.specification[descriptor]);
+  } else {
+    type = makeCategoryType(descriptor);
+  }
+
+  if (type)
+    client.types.set(type.name, type);
+  else
+    throw TypeError("Invalid type: " + descriptor);
+};
+
+
+var makeCompoundType = name => {
+  var index = name.indexOf(":");
+  var [baseType, subType] = [name.slice(0, index), parts.slice(1)];
+  return baseType === "array" ? new ArrayOf(subType) :
+         baseType === "nullable" ? new Maybe(subType) :
+         null;
+};
+
+var makeCategoryType = (descriptor) => {
+  var { category } = descriptor;
+  return category === "dict" ? new Dictionary(descriptor) :
+         category === "actor" ? new Actor(descriptor) :
+         null;
+};
+
+
+var typeFor = (client, type="primitive") => {
+  if (!client.types.has(type))
+    defineType(client, type);
+
+  return client.types.get(type);
+};
+
+
+var Client = Class({
+  constructor: function() {
+  },
+  setupTypes: function(specification) {
+    this.specification = specification;
+    this.types = new Map();
+  },
+  read: function(input, type) {
+    return typeFor(this, type).read(input, this);
+  },
+  write: function(input, type) {
+    return typeFor(this, type).write(input, this);
+  }
+});
+
+
+var Type = Class({
+  get name() {
+    return this.category ? this.category + ":" + this.type :
+           this.type;
+  },
+  read: function(input, client) {
+    throw new TypeError("`Type` subclass must implement `read`");
+  },
+  write: function(input, client) {
+    throw new TypeError("`Type` subclass must implement `write`");
+  }
+});
+
+
+var Primitve = Class({
+  extends: Type,
+  constuctor: function(type) {
+    this.type = type;
+  },
+  read: function(input, client) {
+    return input;
+  },
+  write: function(input, client) {
+    return input;
+  }
+});
+
+var Maybe = Class({
+  extends: Type,
+  category: "nullable",
+  constructor: function(type) {
+    this.type = type;
+  },
+  read: function(input, client) {
+    return input === null ? null :
+           input === void(0) ? void(0) :
+           client.read(input, this.type);
+  },
+  write: function(input, client) {
+    return input === null ? null :
+           input === void(0) ? void(0) :
+           client.write(input, this.type);
+  }
+});
+
+var ArrayOf = Class({
+  extends: Type,
+  category: "array",
+  constructor: function(type) {
+    this.type = type;
+  },
+  read: function(input, client) {
+    return input.map($ => client.read($, this.type));
+  },
+  write: function(input, client) {
+    return input.map($ => client.write($, this.type));
+  }
+});
+
+var Dictionary = Class({
+  exteds: Type,
+  category: "dict",
+  get name() { return this.type; },
+  constructor: function({typeName, specializations}) {
+    this.type = typeName;
+    this.types = specifications;
+  },
+  read: function(input, client) {
+    var output = {};
+    for (var key in input) {
+      output[key] = client.read(input[key], this.types[key]);
+    }
+    return output;
+  },
+  write: function(input, client) {
+    var output = {};
+    for (var key in input) {
+      output[key] = client.write(value, this.types[key]);
+    }
+    return output;
+  }
+});
+
+var Actor = Class({
+  exteds: Type,
+  category: "actor",
+  get name() { return this.type; },
+  constructor: function({typeName}) {
+    this.type = typeName;
+  },
+  read: function(input, client, detail) {
+    var id = value.actor;
+    var actor = void(0);
+    if (client.connection.has(id)) {
+      return client.connection.get(id).form(input, detail, client);
+    } else {
+      actor = Client.from(detail, client);
+      actor.actorID = id;
+      client.supervise(actor);
+    }
+  },
+  write: function(input, client, detail) {
+    if (input instanceof Actor) {
+      if (!input.actorID) {
+        client.supervise(input);
+      }
+      return input.from(detail);
+    }
+    return input.actorID;
+  }
+});
+
+var Root = Client.from({
+  "category": "actor",
+  "typeName": "root",
+  "methods": [
+    {"name": "listTabs",
+     "request": {},
+     "response": {
+     }
+    },
+    {"name": "listAddons"
+    },
+    {"name": "echo",
+
+    },
+    {"name": "protocolDescription",
+
+    }
+  ]
+});
+
+
+var ActorDetail = Class({
+  extends: Actor,
+  constructor: function(name, actor, detail) {
+    this.detail = detail;
+    this.actor = actor;
+  },
+  read: function(input, client) {
+    this.actor.read(input, client, this.detail);
+  },
+  write: function(input, client) {
+    this.actor.write(input, client, this.detail);
+  }
+});
+
+var registeredLifetimes = new Map();
+var LifeTime = Class({
+  extends: Type,
+  category: "lifetime",
+  constructor: function(lifetime, type) {
+    this.name = lifetime + ":" + type.name;
+    this.field = registeredLifetimes.get(lifetime);
+  },
+  read: function(input, client) {
+    return this.type.read(input, client[this.field]);
+  },
+  write: function(input, client) {
+    return this.type.write(input, client[this.field]);
+  }
+});
+
+var primitive = new Primitve("primitive");
+var string = new Primitve("string");
+var number = new Primitve("number");
+var boolean = new Primitve("boolean");
+var json = new Primitve("json");
+var array = new Primitve("array");
+
+
+var TypedValue = Class({
+  extends: Type,
+  constructor: function(name, type) {
+    this.TypedValue(name, type);
+  },
+  TypedValue: function(name, type) {
+    this.name = name;
+    this.type = type;
+  },
+  read: function(input, client) {
+    return this.client.read(input, this.type);
+  },
+  write: function(input, client) {
+    return this.client.write(input, this.type);
+  }
+});
+
+var Return = Class({
+  extends: TypedValue,
+  constructor: function(type) {
+    this.type = type
+  }
+});
+
+var Argument = Class({
+  extends: TypedValue,
+  constructor: function(...args) {
+    this.Argument(...args);
+  },
+  Argument: function(index, type) {
+    this.index = index;
+    this.TypedValue("argument[" + index + "]", type);
+  },
+  read: function(input, client, target) {
+    return target[this.index] = client.read(input, this.type);
+  }
+});
+
+var Option = Class({
+  extends: Argument,
+  constructor: function(...args) {
+    return this.Argument(...args);
+  },
+  read: function(input, client, target, name) {
+    var param = target[this.index] || (target[this.index] = {});
+    param[name] = input === void(0) ? input : client.read(input, this.type);
+  },
+  write: function(input, client, name) {
+    var value = input && input[name];
+    return value === void(0) ? value : client.write(value, this.type);
+  }
+});
+
+var Request = Class({
+  extends: Type,
+  constructor: function(template={}) {
+    this.type = template.type;
+    this.template = template;
+    this.params = findPlaceholders(template, Argument);
+  },
+  read: function(packet, client) {
+    var args = [];
+    for (var param of this.params) {
+      var {placeholder, path} = param;
+      var name = path[path.length - 1];
+      placeholder.read(getPath(packet, path), client, args, name);
+      // TODO:
+      // args[placeholder.index] = placeholder.read(query(packet, path), client);
+    }
+    return args;
+  },
+  write: function(input, client) {
+    return JSON.parse(JSON.stringify(this.template, (key, value) => {
+      return value instanceof Argument ? value.write(input[value.index],
+                                                     client, key) :
+             value;
+    }));
+  }
+});
+
+var Response = Class({
+  extends: Type,
+  constructor: function(template={}) {
+    this.template = template;
+    var [x] = findPlaceholders(template, Return);
+    var {placeholder, path} = x;
+    this.return = placeholder;
+    this.path = path;
+  },
+  read: function(packet, client) {
+    var value = query(packet, this.path);
+    return this.return.read(value, client);
+  },
+  write: function(input, client) {
+    return JSON.parse(JSON.stringify(this.template, (key, value) => {
+      return value instanceof Return ? value.write(input) :
+             input
+    }));
+  }
+});
+
+// Returns array of values for the given object.
+var values = object => Object.keys(object).map(key => object[key]);
+// Returns [key, value] pairs for the given object.
+var pairs = object => Object.keys(object).map(key => [key, object[key]]);
+// Queries an object for the field nested with in it.
+var query = (object, path) => path.reduce((object, entry) => object && object[entry],
+                                          object);
+
+
+var Root = Client.from({
+  "category": "actor",
+  "typeName": "root",
+  "methods": [
+    {
+      "name": "echo",
+      "request": {
+        "string": { "_arg": 0, "type": "string" }
+      },
+      "response": {
+        "string": { "_retval": "string" }
+      }
+    },
+    {
+      "name": "listTabs",
+      "request": {},
+      "response": { "_retval": "tablist" }
+    },
+    {
+      "name": "actorDescriptions",
+      "request": {},
+      "response": { "_retval": "json" }
+    }
+  ],
+  "events": {
+    "tabListChanged": {}
+  }
+});
+
+var Tab = Client.from({
+  "category": "dict",
+  "typeName": "tab",
+  "specifications": {
+    "title": "string",
+    "url": "string",
+    "outerWindowID": "number",
+    "console": "console",
+    "inspectorActor": "inspector",
+    "callWatcherActor": "call-watcher",
+    "canvasActor": "canvas",
+    "webglActor": "webgl",
+    "webaudioActor": "webaudio",
+    "styleSheetsActor": "stylesheets",
+    "styleEditorActor": "styleeditor",
+    "storageActor": "storage",
+    "gcliActor": "gcli",
+    "memoryActor": "memory",
+    "eventLoopLag": "eventLoopLag",
+
+    "trace": "trace", // missing
+  }
+});
+
+var tablist = Client.from({
+  "category": "dict",
+  "typeName": "tablist",
+  "specializations": {
+    "selected": "number",
+    "tabs": "array:tab"
+  }
+});
+
+})(this);
+
diff --git a/addon-sdk/source/examples/debug-client/data/index.html b/addon-sdk/source/examples/debug-client/data/index.html
new file mode 100644
index 000000000..7788e3580
--- /dev/null
+++ b/addon-sdk/source/examples/debug-client/data/index.html
@@ -0,0 +1,50 @@
+
+
+  
+      
+      
+  
+  
+  
+  
+
diff --git a/addon-sdk/source/examples/debug-client/data/plugin.png b/addon-sdk/source/examples/debug-client/data/plugin.png
new file mode 100644
index 000000000..6a364a30a
Binary files /dev/null and b/addon-sdk/source/examples/debug-client/data/plugin.png differ
diff --git a/addon-sdk/source/examples/debug-client/data/task.js b/addon-sdk/source/examples/debug-client/data/task.js
new file mode 100644
index 000000000..b46feb10e
--- /dev/null
+++ b/addon-sdk/source/examples/debug-client/data/task.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/. */
+
+(function(exports) {
+"use strict";
+
+const spawn = (task, ...args) => {
+  return new Promise((resolve, reject) => {
+    try {
+      const routine = task(...args);
+      const raise = error => routine.throw(error);
+      const step = data => {
+        const { done, value } = routine.next(data);
+        if (done)
+          resolve(value);
+        else
+          Promise.resolve(value).then(step, raise);
+      }
+      step();
+    } catch(error) {
+      reject(error);
+    }
+  });
+}
+exports.spawn = spawn;
+
+})(Task = {});
diff --git a/addon-sdk/source/examples/debug-client/index.js b/addon-sdk/source/examples/debug-client/index.js
new file mode 100644
index 000000000..ff91ff8cd
--- /dev/null
+++ b/addon-sdk/source/examples/debug-client/index.js
@@ -0,0 +1,33 @@
+/* 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 { Panel } = require("dev/panel");
+const { Tool } = require("dev/toolbox");
+const { Class } = require("sdk/core/heritage");
+
+
+const LadybugPanel = Class({
+  extends: Panel,
+  label: "Ladybug",
+  tooltip: "Debug client example",
+  icon: "./plugin.png",
+  url: "./index.html",
+  setup: function({debuggee}) {
+    this.debuggee = debuggee;
+  },
+  dispose: function() {
+    delete this.debuggee;
+  },
+  onReady: function() {
+    this.debuggee.start();
+    this.postMessage("RDP", [this.debuggee]);
+  },
+});
+exports.LadybugPanel = LadybugPanel;
+
+
+const ladybug = new Tool({
+  panels: { ladybug: LadybugPanel }
+});
diff --git a/addon-sdk/source/examples/debug-client/package.json b/addon-sdk/source/examples/debug-client/package.json
new file mode 100644
index 000000000..058fa97af
--- /dev/null
+++ b/addon-sdk/source/examples/debug-client/package.json
@@ -0,0 +1,10 @@
+{
+  "name": "debug-client",
+  "id": "@debug-client",
+  "title": "Debug client",
+  "description": "Example debug client",
+  "version": "0.0.1",
+  "author": "Irakli Gozalishvili",
+  "main": "./index.js",
+  "license": "MPL-2.0"
+}
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 000000000..9862fc20b
--- /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/reading-data/data/mom.png b/addon-sdk/source/examples/reading-data/data/mom.png
new file mode 100644
index 000000000..4ba89a2c1
Binary files /dev/null and b/addon-sdk/source/examples/reading-data/data/mom.png differ
diff --git a/addon-sdk/source/examples/reading-data/data/sample.html b/addon-sdk/source/examples/reading-data/data/sample.html
new file mode 100644
index 000000000..c7c09cb98
--- /dev/null
+++ b/addon-sdk/source/examples/reading-data/data/sample.html
@@ -0,0 +1,7 @@
+
+
+
+

Hello World

+ diff --git a/addon-sdk/source/examples/reading-data/lib/main.js b/addon-sdk/source/examples/reading-data/lib/main.js new file mode 100644 index 000000000..468a497b1 --- /dev/null +++ b/addon-sdk/source/examples/reading-data/lib/main.js @@ -0,0 +1,53 @@ +/* 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 self = require("sdk/self"); +var { Panel } = require("sdk/panel"); +var { ToggleButton } = require("sdk/ui"); + +function replaceMom(html) { + return html.replace("World", "Mom"); +} +exports.replaceMom = replaceMom; + +exports.main = function(options, callbacks) { + console.log("My ID is " + self.id); + + // Load the sample HTML into a string. + var helloHTML = self.data.load("sample.html"); + + // Let's now modify it... + helloHTML = replaceMom(helloHTML); + + // ... and then create a panel that displays it. + var myPanel = Panel({ + contentURL: "data:text/html," + helloHTML, + onHide: handleHide + }); + + // Create a widget that displays the image. We'll attach the panel to it. + // When you click the widget, the panel will pop up. + var button = ToggleButton({ + id: "test-widget", + label: "Mom", + icon: './mom.png', + onChange: handleChange + }); + + // If you run cfx with --static-args='{"quitWhenDone":true}' this program + // will automatically quit Firefox when it's done. + if (options.staticArgs.quitWhenDone) + callbacks.quit(); +} + +function handleChange(state) { + if (state.checked) { + myPanel.show({ position: button }); + } +} + +function handleHide() { + button.state('window', { checked: false }); +} diff --git a/addon-sdk/source/examples/reading-data/package.json b/addon-sdk/source/examples/reading-data/package.json new file mode 100644 index 000000000..8bdce6423 --- /dev/null +++ b/addon-sdk/source/examples/reading-data/package.json @@ -0,0 +1,9 @@ +{ + "name": "reading-data", + "description": "A demonstration of reading bundled data.", + "keywords": [], + "author": "Brian Warner", + "contributors": [], + "license": "MPL-2.0", + "id": "reading-data-example@jetpack.mozillalabs.com" +} diff --git a/addon-sdk/source/examples/reading-data/tests/test-main.js b/addon-sdk/source/examples/reading-data/tests/test-main.js new file mode 100644 index 000000000..4e85f49de --- /dev/null +++ b/addon-sdk/source/examples/reading-data/tests/test-main.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"; + +var m = require("main"); +var self = require("sdk/self"); + +exports.testReplace = function(test) { + var input = "Hello World"; + var output = m.replaceMom(input); + test.assertEqual(output, "Hello Mom"); + var callbacks = { quit: function() {} }; + + // Make sure it doesn't crash... + m.main({ staticArgs: {} }, callbacks); +}; + +exports.testID = function(test) { + // The ID is randomly generated during tests, so we cannot compare it against + // anything in particular. Just assert that it is not empty. + test.assert(self.id.length > 0); + test.assertEqual(self.data.url("sample.html"), + "resource://reading-data-example-at-jetpack-dot-mozillalabs-dot-com/reading-data/data/sample.html"); +}; diff --git a/addon-sdk/source/examples/theme/data/icon-16.png b/addon-sdk/source/examples/theme/data/icon-16.png new file mode 100644 index 000000000..72327f77b Binary files /dev/null and b/addon-sdk/source/examples/theme/data/icon-16.png differ diff --git a/addon-sdk/source/examples/theme/data/index.html b/addon-sdk/source/examples/theme/data/index.html new file mode 100644 index 000000000..24ffc1a04 --- /dev/null +++ b/addon-sdk/source/examples/theme/data/index.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/addon-sdk/source/examples/theme/data/theme.css b/addon-sdk/source/examples/theme/data/theme.css new file mode 100644 index 000000000..c837960d9 --- /dev/null +++ b/addon-sdk/source/examples/theme/data/theme.css @@ -0,0 +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/. */ + +#devtools-theme-box { + background-color: red !important; +} diff --git a/addon-sdk/source/examples/theme/lib/main.js b/addon-sdk/source/examples/theme/lib/main.js new file mode 100644 index 000000000..3b71376a5 --- /dev/null +++ b/addon-sdk/source/examples/theme/lib/main.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"; + +const { Tool } = require("dev/toolbox"); +const { Class } = require("sdk/core/heritage"); +const { onEnable, onDisable } = require("dev/theme/hooks"); +const { Theme, LightTheme } = require("dev/theme"); + +/** + * This object represents a new theme registered within the Toolbox. + * You can activate it by clicking on "My Light Theme" theme option + * in the Options panel. + * Note that the new theme derives styles from built-in Light theme. + */ +const MyTheme = Theme({ + name: "mytheme", + label: "My Light Theme", + styles: [LightTheme, "./theme.css"], + + onEnable: function(window, oldTheme) { + console.log("myTheme.onEnable; method override " + + window.location.href); + }, + onDisable: function(window, newTheme) { + console.log("myTheme.onDisable; method override " + + window.location.href); + }, +}); + +// Registration + +const mytheme = new Tool({ + name: "My Tool", + themes: { mytheme: MyTheme } +}); diff --git a/addon-sdk/source/examples/theme/package.json b/addon-sdk/source/examples/theme/package.json new file mode 100644 index 000000000..bb18a78ae --- /dev/null +++ b/addon-sdk/source/examples/theme/package.json @@ -0,0 +1,10 @@ +{ + "name": "theme", + "title": "theme", + "id": "theme@jetpack", + "description": "How to create new theme for devtools", + "author": "Jan Odvarko", + "license": "MPL-2.0", + "version": "0.1.0", + "main": "lib/main" +} diff --git a/addon-sdk/source/examples/theme/test/test-main.js b/addon-sdk/source/examples/theme/test/test-main.js new file mode 100644 index 000000000..8a5e101d3 --- /dev/null +++ b/addon-sdk/source/examples/theme/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/toolbar-api/data/favicon.ico b/addon-sdk/source/examples/toolbar-api/data/favicon.ico new file mode 100644 index 000000000..ae5084bc0 Binary files /dev/null and b/addon-sdk/source/examples/toolbar-api/data/favicon.ico differ diff --git a/addon-sdk/source/examples/toolbar-api/data/index.html b/addon-sdk/source/examples/toolbar-api/data/index.html new file mode 100644 index 000000000..4c91a63ae --- /dev/null +++ b/addon-sdk/source/examples/toolbar-api/data/index.html @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/addon-sdk/source/examples/toolbar-api/lib/main.js b/addon-sdk/source/examples/toolbar-api/lib/main.js new file mode 100644 index 000000000..a538c8cd6 --- /dev/null +++ b/addon-sdk/source/examples/toolbar-api/lib/main.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"; + +const { Toolbar } = require("sdk/ui/toolbar"); +const { Frame } = require("sdk/ui/frame"); +const { ActionButton } = require("sdk/ui/button/action"); + +var button = new ActionButton({ + id: "button", + label: "send!", + icon: "./favicon.ico", + onClick: () => { + frame.postMessage({ + hello: "content" + }); + } +}); + +var frame = new Frame({ + url: "./index.html", + onAttach: () => { + console.log("frame was attached"); + }, + onReady: () => { + console.log("frame document was loaded"); + }, + onLoad: () => { + console.log("frame load complete"); + }, + onMessage: (event) => { + console.log("got message from frame content", event); + if (event.data === "ping!") + event.source.postMessage("pong!", event.source.origin); + } +}); +var toolbar = new Toolbar({ + items: [frame], + title: "Addon Demo", + hidden: false, + onShow: () => { + console.log("toolbar was shown"); + }, + onHide: () => { + console.log("toolbar was hidden"); + } +}); diff --git a/addon-sdk/source/examples/toolbar-api/package.json b/addon-sdk/source/examples/toolbar-api/package.json new file mode 100644 index 000000000..62af7f7ff --- /dev/null +++ b/addon-sdk/source/examples/toolbar-api/package.json @@ -0,0 +1,12 @@ +{ + "name": "toolbar-api", + "title": "Toolbar API", + "main": "./lib/main.js", + "description": "a toolbar api example", + "author": "", + "license": "MPL-2.0", + "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 000000000..9862fc20b --- /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/lib/main.js b/addon-sdk/source/examples/ui-button-apis/lib/main.js new file mode 100644 index 000000000..f0ae3dd6c --- /dev/null +++ b/addon-sdk/source/examples/ui-button-apis/lib/main.js @@ -0,0 +1,39 @@ +/* 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 data = require('sdk/self').data; +var tabs = require('sdk/tabs'); +var { notify } = require('sdk/notifications'); +var { ActionButton, ToggleButton } = require('sdk/ui'); + +var icon = 'chrome://mozapps/skin/extensions/extensionGeneric.svg'; +exports.icon = icon; + +// your basic action button +var action = ActionButton({ + id: 'test-action-button', + label: 'Action Button', + icon: icon, + onClick: function (state) { + notify({ + title: "Action!", + text: "This notification was triggered from an action button!", + }); + } +}); +exports.actionButton = action; + +var toggle = ToggleButton({ + id: 'test-toggle-button', + label: 'Toggle Button', + icon: icon, + onClick: function (state) { + notify({ + title: "Toggled!", + text: "The current state of the button is " + state.checked, + }); + } +}); +exports.toggleButton = toggle; diff --git a/addon-sdk/source/examples/ui-button-apis/package.json b/addon-sdk/source/examples/ui-button-apis/package.json new file mode 100644 index 000000000..83cf7f6ff --- /dev/null +++ b/addon-sdk/source/examples/ui-button-apis/package.json @@ -0,0 +1,10 @@ +{ + "name": "ui-button-apis", + "title": "Australis Button API Examples", + "id": "ui-button-apis@mozilla.org", + "description": "A Button API example", + "author": "jeff@canuckistani.ca (Jeff Griffiths | @canuckistani)", + "license": "MPL-2.0", + "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 new file mode 100644 index 000000000..49bdc863a --- /dev/null +++ b/addon-sdk/source/examples/ui-button-apis/tests/test-main.js @@ -0,0 +1,29 @@ +/* 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"; + +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) { + assert.equal(actionButton.id, "test-action-button", "action button id is correct"); + assert.equal(actionButton.label, "Action Button", "action button label is correct"); + assert.equal(actionButton.icon, icon, "action button icon is correct"); +} + +exports.testToggleButton = function(assert) { + assert.equal(toggleButton.id, "test-toggle-button", "toggle button id is correct"); + assert.equal(toggleButton.label, "Toggle Button", "toggle button label is correct"); + assert.equal(toggleButton.icon, icon, "toggle button icon is correct"); +} + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/gulpfile.js b/addon-sdk/source/gulpfile.js new file mode 100644 index 000000000..4020dd9d4 --- /dev/null +++ b/addon-sdk/source/gulpfile.js @@ -0,0 +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"; + +var gulp = require('gulp'); +var patch = require("./bin/node-scripts/apply-patch"); +var ini = require("./bin/node-scripts/update-ini"); + +gulp.task('test', function(done) { + require("./bin/jpm-test").run().then(done); +}); + +gulp.task('test:addons', function(done) { + require("./bin/jpm-test").run("addons").catch(console.error).then(done); +}); + +gulp.task('test:docs', function(done) { + require("./bin/jpm-test").run("docs").catch(console.error).then(done); +}); + +gulp.task('test:examples', function(done) { + require("./bin/jpm-test").run("examples").catch(console.error).then(done); +}); + +gulp.task('test:modules', function(done) { + require("./bin/jpm-test").run("modules").catch(console.error).then(done); +}); + +gulp.task('test:ini', function(done) { + require("./bin/jpm-test").run("ini").catch(console.error).then(done); +}); + +gulp.task('test:firefox-bin', function(done) { + require("./bin/jpm-test").run("firefox-bin").catch(console.error).then(done); +}); + +gulp.task('patch:clean', function(done) { + patch.clean().catch(console.error).then(done); +}); + +gulp.task('patch:apply', function(done) { + patch.apply().catch(console.error).then(done); +}); diff --git a/addon-sdk/source/lib/dev/debuggee.js b/addon-sdk/source/lib/dev/debuggee.js new file mode 100644 index 000000000..0ca0bd37a --- /dev/null +++ b/addon-sdk/source/lib/dev/debuggee.js @@ -0,0 +1,95 @@ +/* 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 { Cu } = require("chrome"); +const { Class } = require("../sdk/core/heritage"); +const { MessagePort, MessageChannel } = require("../sdk/messaging"); +const { require: devtoolsRequire } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const { DebuggerServer } = devtoolsRequire("devtools/server/main"); + +const outputs = new WeakMap(); +const inputs = new WeakMap(); +const targets = new WeakMap(); +const transports = new WeakMap(); + +const inputFor = port => inputs.get(port); +const outputFor = port => outputs.get(port); +const transportFor = port => transports.get(port); + +const fromTarget = target => { + const debuggee = new Debuggee(); + const { port1, port2 } = new MessageChannel(); + inputs.set(debuggee, port1); + outputs.set(debuggee, port2); + targets.set(debuggee, target); + + return debuggee; +}; +exports.fromTarget = fromTarget; + +const Debuggee = Class({ + extends: MessagePort.prototype, + close: function() { + const server = transportFor(this); + if (server) { + transports.delete(this); + server.close(); + } + outputFor(this).close(); + }, + start: function() { + const target = targets.get(this); + if (target.isLocalTab) { + // Since a remote protocol connection will be made, let's start the + // DebuggerServer here, once and for all tools. + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + transports.set(this, DebuggerServer.connectPipe()); + } + // TODO: Implement support for remote connections (See Bug 980421) + else { + throw Error("Remote targets are not yet supported"); + } + + // pipe messages send to the debuggee to an actual + // server via remote debugging protocol transport. + inputFor(this).addEventListener("message", ({data}) => + transportFor(this).send(data)); + + // pipe messages received from the remote debugging + // server transport onto the this debuggee. + transportFor(this).hooks = { + onPacket: packet => inputFor(this).postMessage(packet), + onClosed: () => inputFor(this).close() + }; + + inputFor(this).start(); + outputFor(this).start(); + }, + postMessage: function(data) { + return outputFor(this).postMessage(data); + }, + get onmessage() { + return outputFor(this).onmessage; + }, + set onmessage(onmessage) { + outputFor(this).onmessage = onmessage; + }, + addEventListener: function(...args) { + return outputFor(this).addEventListener(...args); + }, + removeEventListener: function(...args) { + return outputFor(this).removeEventListener(...args); + } +}); +exports.Debuggee = Debuggee; diff --git a/addon-sdk/source/lib/dev/frame-script.js b/addon-sdk/source/lib/dev/frame-script.js new file mode 100644 index 000000000..33a197419 --- /dev/null +++ b/addon-sdk/source/lib/dev/frame-script.js @@ -0,0 +1,120 @@ +/* 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"; +(function({content, sendSyncMessage, addMessageListener, sendAsyncMessage}) { + +const Cc = Components.classes; +const Ci = Components.interfaces; +const observerService = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + +const channels = new Map(); +const handles = new WeakMap(); + +// Takes remote port handle and creates a local one. +// also set's up a messaging channel between them. +// This is temporary workaround until Bug 914974 is fixed +// and port can be transfered through message manager. +const demarshal = (handle) => { + if (handle.type === "MessagePort") { + if (!channels.has(handle.id)) { + const channel = new content.MessageChannel(); + channels.set(handle.id, channel); + handles.set(channel.port1, handle); + channel.port1.onmessage = onOutPort; + } + return channels.get(handle.id).port2; + } + return null; +}; + +const onOutPort = event => { + const handle = handles.get(event.target); + sendAsyncMessage("sdk/port/message", { + port: handle, + message: event.data + }); +}; + +const onInPort = ({data}) => { + const channel = channels.get(data.port.id); + if (channel) + channel.port1.postMessage(data.message); +}; + +const onOutEvent = event => + sendSyncMessage("sdk/event/" + event.type, + { type: event.type, + data: event.data }); + +const onInMessage = (message) => { + const {type, data, origin, bubbles, cancelable, ports} = message.data; + + const event = new content.MessageEvent(type, { + bubbles: bubbles, + cancelable: cancelable, + data: data, + origin: origin, + target: content, + source: content, + ports: ports.map(demarshal) + }); + content.dispatchEvent(event); +}; + +const onReady = event => { + channels.clear(); +}; + +addMessageListener("sdk/event/message", onInMessage); +addMessageListener("sdk/port/message", onInPort); + +const observer = { + handleEvent: ({target, type}) => { + observer.observe(target, type); + }, + observe: (document, topic, data) => { + // When frame associated with message manager is removed from document `docShell` + // is set to `null` but observer is still kept alive. At this point accesing + // `content.document` throws "can't access dead object" exceptions. In order to + // avoid leaking observer and logged errors observer is going to be removed when + // `docShell` is set to `null`. + if (!docShell) { + observerService.removeObserver(observer, topic); + } + else if (document === content.document) { + if (topic.endsWith("-document-interactive")) { + sendAsyncMessage("sdk/event/ready", { + type: "ready", + readyState: document.readyState, + uri: document.documentURI + }); + } + if (topic.endsWith("-document-loaded")) { + sendAsyncMessage("sdk/event/load", { + type: "load", + readyState: document.readyState, + uri: document.documentURI + }); + } + if (topic === "unload") { + channels.clear(); + sendAsyncMessage("sdk/event/unload", { + type: "unload", + readyState: "uninitialized", + uri: document.documentURI + }); + } + } + } +}; + +observerService.addObserver(observer, "content-document-interactive", false); +observerService.addObserver(observer, "content-document-loaded", false); +observerService.addObserver(observer, "chrome-document-interactive", false); +observerService.addObserver(observer, "chrome-document-loaded", false); +addEventListener("unload", observer, false); + +})(this); diff --git a/addon-sdk/source/lib/dev/panel.js b/addon-sdk/source/lib/dev/panel.js new file mode 100644 index 000000000..1ef6a303a --- /dev/null +++ b/addon-sdk/source/lib/dev/panel.js @@ -0,0 +1,259 @@ +/* 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 { Cu } = require("chrome"); +const { Class } = require("../sdk/core/heritage"); +const { curry } = require("../sdk/lang/functional"); +const { EventTarget } = require("../sdk/event/target"); +const { Disposable, setup, dispose } = require("../sdk/core/disposable"); +const { emit, off, setListeners } = require("../sdk/event/core"); +const { when } = require("../sdk/event/utils"); +const { getFrameElement } = require("../sdk/window/utils"); +const { contract, validate } = require("../sdk/util/contract"); +const { data: { url: resolve }} = require("../sdk/self"); +const { identify } = require("../sdk/ui/id"); +const { isLocalURL, URL } = require("../sdk/url"); +const { encode } = require("../sdk/base64"); +const { marshal, demarshal } = require("./ports"); +const { fromTarget } = require("./debuggee"); +const { removed } = require("../sdk/dom/events"); +const { id: addonID } = require("../sdk/self"); +const { viewFor } = require("../sdk/view/core"); +const { createView } = require("./panel/view"); + +const OUTER_FRAME_URI = module.uri.replace(/\.js$/, ".html"); +const FRAME_SCRIPT = module.uri.replace("/panel.js", "/frame-script.js"); +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +const makeID = name => + ("dev-panel-" + addonID + "-" + name). + split("/").join("-"). + split(".").join("-"). + split(" ").join("-"). + replace(/[^A-Za-z0-9_\-]/g, ""); + + +// Weak mapping between `Panel` instances and their frame's +// `nsIMessageManager`. +const managers = new WeakMap(); +// Return `nsIMessageManager` for the given `Panel` instance. +const managerFor = x => managers.get(x); + +// Weak mappinging between iframe's and their owner +// `Panel` instances. +const panels = new WeakMap(); +const panelFor = frame => panels.get(frame); + +// Weak mapping between panels and debugees they're targeting. +const debuggees = new WeakMap(); +const debuggeeFor = panel => debuggees.get(panel); + +const frames = new WeakMap(); +const frameFor = panel => frames.get(panel); + +const setAttributes = (node, attributes) => { + for (var key in attributes) + node.setAttribute(key, attributes[key]); +}; + +const onStateChange = ({target, data}) => { + const panel = panelFor(target); + panel.readyState = data.readyState; + emit(panel, data.type, { target: panel, type: data.type }); +}; + +// port event listener on the message manager that demarshalls +// and forwards to the actual receiver. This is a workaround +// until Bug 914974 is fixed. +const onPortMessage = ({data, target}) => { + const port = demarshal(target, data.port); + if (port) + port.postMessage(data.message); +}; + +// When frame is removed from the toolbox destroy panel +// associated with it to release all the resources. +const onFrameRemove = frame => { + panelFor(frame).destroy(); +}; + +const onFrameInited = frame => { + frame.style.visibility = "visible"; +} + +const inited = frame => new Promise(resolve => { + const { messageManager } = frame.frameLoader; + const listener = message => { + messageManager.removeMessageListener("sdk/event/ready", listener); + resolve(frame); + }; + messageManager.addMessageListener("sdk/event/ready", listener); +}); + +const getTarget = ({target}) => target; + +const Panel = Class({ + extends: Disposable, + implements: [EventTarget], + get id() { + return makeID(this.name || this.label); + }, + readyState: "uninitialized", + ready: function() { + const { readyState } = this; + const isReady = readyState === "complete" || + readyState === "interactive"; + return isReady ? Promise.resolve(this) : + when(this, "ready").then(getTarget); + }, + loaded: function() { + const { readyState } = this; + const isLoaded = readyState === "complete"; + return isLoaded ? Promise.resolve(this) : + when(this, "load").then(getTarget); + }, + unloaded: function() { + const { readyState } = this; + const isUninitialized = readyState === "uninitialized"; + return isUninitialized ? Promise.resolve(this) : + when(this, "unload").then(getTarget); + }, + postMessage: function(data, ports=[]) { + const manager = managerFor(this); + manager.sendAsyncMessage("sdk/event/message", { + type: "message", + bubbles: false, + cancelable: false, + data: data, + origin: this.url, + ports: ports.map(marshal(manager)) + }); + } +}); +exports.Panel = Panel; + +validate.define(Panel, contract({ + label: { + is: ["string"], + msg: "The `option.label` must be a provided" + }, + tooltip: { + is: ["string", "undefined"], + msg: "The `option.tooltip` must be a string" + }, + icon: { + is: ["string"], + map: x => x && resolve(x), + ok: x => isLocalURL(x), + msg: "The `options.icon` must be a valid local URI." + }, + url: { + map: x => resolve(x.toString()), + is: ["string"], + ok: x => isLocalURL(x), + msg: "The `options.url` must be a valid local URI." + }, + invertIconForLightTheme: { + is: ["boolean", "undefined"], + msg: "The `options.invertIconForLightTheme` must be a boolean." + }, + invertIconForDarkTheme: { + is: ["boolean", "undefined"], + msg: "The `options.invertIconForDarkTheme` must be a boolean." + } +})); + +setup.define(Panel, (panel, {window, toolbox, url}) => { + // Hack: Given that iframe created by devtools API is no good for us, + // we obtain original iframe and replace it with the one that has + // desired configuration. + const original = getFrameElement(window); + const container = original.parentNode; + original.remove(); + const frame = createView(panel, container.ownerDocument); + + // Following modifications are a temporary workaround until Bug 1049188 + // is fixed. + // Enforce certain iframe customizations regardless of users request. + setAttributes(frame, { + "id": original.id, + "src": url, + "flex": 1, + "forceOwnRefreshDriver": "", + "tooltip": "aHTMLTooltip" + }); + frame.style.visibility = "hidden"; + frame.classList.add("toolbox-panel-iframe"); + // Inject iframe into designated node until add-on author decides + // to inject it elsewhere instead. + if (!frame.parentNode) + container.appendChild(frame); + + // associate view with a panel + frames.set(panel, frame); + + // associate panel model with a frame view. + panels.set(frame, panel); + + const debuggee = fromTarget(toolbox.target); + // associate debuggee with a panel. + debuggees.set(panel, debuggee); + + + // Setup listeners for the frame message manager. + const { messageManager } = frame.frameLoader; + messageManager.addMessageListener("sdk/event/ready", onStateChange); + messageManager.addMessageListener("sdk/event/load", onStateChange); + messageManager.addMessageListener("sdk/event/unload", onStateChange); + messageManager.addMessageListener("sdk/port/message", onPortMessage); + messageManager.loadFrameScript(FRAME_SCRIPT, false); + + managers.set(panel, messageManager); + + // destroy panel if frame is removed. + removed(frame).then(onFrameRemove); + // show frame when it is initialized. + inited(frame).then(onFrameInited); + + + // set listeners if there are ones defined on the prototype. + setListeners(panel, Object.getPrototypeOf(panel)); + + + panel.setup({ debuggee: debuggee }); +}); + +createView.define(Panel, (panel, document) => { + const frame = document.createElement("iframe"); + setAttributes(frame, { + "sandbox": "allow-scripts", + // We end up using chrome iframe with forced message manager + // as fixing a swapFrameLoader seemed like a giant task (see + // Bug 1075490). + "type": "chrome", + "forcemessagemanager": true, + "transparent": true, + "seamless": "seamless", + }); + return frame; +}); + +dispose.define(Panel, function(panel) { + debuggeeFor(panel).close(); + + debuggees.delete(panel); + managers.delete(panel); + frames.delete(panel); + panel.readyState = "destroyed"; + panel.dispose(); +}); + +viewFor.define(Panel, frameFor); diff --git a/addon-sdk/source/lib/dev/panel/view.js b/addon-sdk/source/lib/dev/panel/view.js new file mode 100644 index 000000000..41cf9c221 --- /dev/null +++ b/addon-sdk/source/lib/dev/panel/view.js @@ -0,0 +1,14 @@ +/* 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 { method } = require("method/core"); + +const createView = method("dev/panel/view#createView"); +exports.createView = createView; diff --git a/addon-sdk/source/lib/dev/ports.js b/addon-sdk/source/lib/dev/ports.js new file mode 100644 index 000000000..a41f59eb7 --- /dev/null +++ b/addon-sdk/source/lib/dev/ports.js @@ -0,0 +1,64 @@ +/* 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" +}; + +// This module provides `marshal` and `demarshal` functions +// that can be used to send MessagePort's over `nsIFrameMessageManager` +// until Bug 914974 is fixed. + +const { add, iterator } = require("../sdk/lang/weak-set"); +const { curry } = require("../sdk/lang/functional"); + +var id = 0; +const ports = new WeakMap(); + +// Takes `nsIFrameMessageManager` and `MessagePort` instances +// and returns a handle representing given `port`. Messages +// received on given `port` will be forwarded to a message +// manager under `sdk/port/message` and messages like: +// { port: { type: "MessagePort", id: 2}, data: data } +// Where id is an identifier associated with a given `port` +// and `data` is an `event.data` received on port. +const marshal = curry((manager, port) => { + if (!ports.has(port)) { + id = id + 1; + const handle = {type: "MessagePort", id: id}; + // Bind id to the given port + ports.set(port, handle); + + // Obtain a weak reference to a port. + add(exports, port); + + port.onmessage = event => { + manager.sendAsyncMessage("sdk/port/message", { + port: handle, + message: event.data + }); + }; + + return handle; + } + return ports.get(port); +}); +exports.marshal = marshal; + +// Takes `nsIFrameMessageManager` instance and a handle returned +// `marshal(manager, port)` returning a `port` that was passed +// to it. Note that `port` may be GC-ed in which case returned +// value will be `null`. +const demarshal = curry((manager, {type, id}) => { + if (type === "MessagePort") { + for (let port of iterator(exports)) { + if (id === ports.get(port).id) + return port; + } + } + return null; +}); +exports.demarshal = demarshal; diff --git a/addon-sdk/source/lib/dev/theme.js b/addon-sdk/source/lib/dev/theme.js new file mode 100644 index 000000000..05930a502 --- /dev/null +++ b/addon-sdk/source/lib/dev/theme.js @@ -0,0 +1,135 @@ +/* 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 { Class } = require("../sdk/core/heritage"); +const { EventTarget } = require("../sdk/event/target"); +const { Disposable, setup, dispose } = require("../sdk/core/disposable"); +const { contract, validate } = require("../sdk/util/contract"); +const { id: addonID } = require("../sdk/self"); +const { onEnable, onDisable } = require("dev/theme/hooks"); +const { isString, instanceOf, isFunction } = require("sdk/lang/type"); +const { add } = require("sdk/util/array"); +const { data } = require("../sdk/self"); +const { isLocalURL } = require("../sdk/url"); + +const makeID = name => + ("dev-theme-" + addonID + (name ? "-" + name : "")). + split(/[ . /]/).join("-"). + replace(/[^A-Za-z0-9_\-]/g, ""); + +const Theme = Class({ + extends: Disposable, + implements: [EventTarget], + + initialize: function(options) { + this.name = options.name; + this.label = options.label; + this.styles = options.styles; + + // Event handlers + this.onEnable = options.onEnable; + this.onDisable = options.onDisable; + }, + get id() { + return makeID(this.name || this.label); + }, + setup: function() { + // Any initialization steps done at the registration time. + }, + getStyles: function() { + if (!this.styles) { + return []; + } + + if (isString(this.styles)) { + if (isLocalURL(this.styles)) { + return [data.url(this.styles)]; + } + } + + let result = []; + for (let style of this.styles) { + if (isString(style)) { + if (isLocalURL(style)) { + style = data.url(style); + } + add(result, style); + } else if (instanceOf(style, Theme)) { + result = result.concat(style.getStyles()); + } + } + return result; + }, + getClassList: function() { + let result = []; + for (let style of this.styles) { + if (instanceOf(style, Theme)) { + result = result.concat(style.getClassList()); + } + } + + if (this.name) { + add(result, this.name); + } + + return result; + } +}); + +exports.Theme = Theme; + +// Initialization & dispose + +setup.define(Theme, (theme) => { + theme.classList = []; + theme.setup(); +}); + +dispose.define(Theme, function(theme) { + theme.dispose(); +}); + +// Validation + +validate.define(Theme, contract({ + label: { + is: ["string"], + msg: "The `option.label` must be a provided" + }, +})); + +// Support theme events: apply and unapply the theme. + +onEnable.define(Theme, (theme, {window, oldTheme}) => { + if (isFunction(theme.onEnable)) { + theme.onEnable(window, oldTheme); + } +}); + +onDisable.define(Theme, (theme, {window, newTheme}) => { + if (isFunction(theme.onDisable)) { + theme.onDisable(window, newTheme); + } +}); + +// Support for built-in themes + +const LightTheme = Theme({ + name: "theme-light", + styles: "chrome://devtools/skin/light-theme.css", +}); + +const DarkTheme = Theme({ + name: "theme-dark", + styles: "chrome://devtools/skin/dark-theme.css", +}); + +exports.LightTheme = LightTheme; +exports.DarkTheme = DarkTheme; diff --git a/addon-sdk/source/lib/dev/theme/hooks.js b/addon-sdk/source/lib/dev/theme/hooks.js new file mode 100644 index 000000000..9987f853b --- /dev/null +++ b/addon-sdk/source/lib/dev/theme/hooks.js @@ -0,0 +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"; + +module.metadata = { + "stability": "experimental" +}; + +const { method } = require("method/core"); + +const onEnable = method("dev/theme/hooks#onEnable"); +const onDisable = method("dev/theme/hooks#onDisable"); + +exports.onEnable = onEnable; +exports.onDisable = onDisable; diff --git a/addon-sdk/source/lib/dev/toolbox.js b/addon-sdk/source/lib/dev/toolbox.js new file mode 100644 index 000000000..43f37759f --- /dev/null +++ b/addon-sdk/source/lib/dev/toolbox.js @@ -0,0 +1,107 @@ +/* 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 { Cu, Cc, Ci } = require("chrome"); +const { Class } = require("../sdk/core/heritage"); +const { Disposable, setup } = require("../sdk/core/disposable"); +const { contract, validate } = require("../sdk/util/contract"); +const { each, pairs, values } = require("../sdk/util/sequence"); +const { onEnable, onDisable } = require("../dev/theme/hooks"); + +const { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {}); + +// This is temporary workaround to allow loading of the developer tools client - volcan +// into a toolbox panel, this hack won't be necessary as soon as devtools patch will be +// shipped in nightly, after which it can be removed. Bug 1038517 +const registerSDKURI = () => { + const ioService = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + const resourceHandler = ioService.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + const uri = module.uri.replace("dev/toolbox.js", ""); + resourceHandler.setSubstitution("sdk", ioService.newURI(uri, null, null)); +}; + +registerSDKURI(); + +const Tool = Class({ + extends: Disposable, + setup: function(params={}) { + const { panels } = validate(this, params); + const { themes } = validate(this, params); + + this.panels = panels; + this.themes = themes; + + each(([key, Panel]) => { + const { url, label, tooltip, icon, invertIconForLightTheme, + invertIconForDarkTheme } = validate(Panel.prototype); + const { id } = Panel.prototype; + + gDevTools.registerTool({ + id: id, + url: "about:blank", + label: label, + tooltip: tooltip, + icon: icon, + invertIconForLightTheme: invertIconForLightTheme, + invertIconForDarkTheme: invertIconForDarkTheme, + isTargetSupported: target => target.isLocalTab, + build: (window, toolbox) => { + const panel = new Panel(); + setup(panel, { window: window, + toolbox: toolbox, + url: url }); + + return panel.ready(); + } + }); + }, pairs(panels)); + + each(([key, theme]) => { + validate(theme); + setup(theme); + + gDevTools.registerTheme({ + id: theme.id, + label: theme.label, + stylesheets: theme.getStyles(), + classList: theme.getClassList(), + onApply: (window, oldTheme) => { + onEnable(theme, { window: window, + oldTheme: oldTheme }); + }, + onUnapply: (window, newTheme) => { + onDisable(theme, { window: window, + newTheme: newTheme }); + } + }); + }, pairs(themes)); + }, + dispose: function() { + each(Panel => gDevTools.unregisterTool(Panel.prototype.id), + values(this.panels)); + + each(Theme => gDevTools.unregisterTheme(Theme.prototype.id), + values(this.themes)); + } +}); + +validate.define(Tool, contract({ + panels: { + is: ["object", "undefined"] + }, + themes: { + is: ["object", "undefined"] + } +})); + +exports.Tool = Tool; diff --git a/addon-sdk/source/lib/dev/utils.js b/addon-sdk/source/lib/dev/utils.js new file mode 100644 index 000000000..48db39e04 --- /dev/null +++ b/addon-sdk/source/lib/dev/utils.js @@ -0,0 +1,40 @@ +/* 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 { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {}); +const { devtools } = Cu.import("resource://devtools/shared/Loader.jsm", {}); + +const { getActiveTab } = require("../sdk/tabs/utils"); +const { getMostRecentBrowserWindow } = require("../sdk/window/utils"); + +const targetFor = target => { + target = target || getActiveTab(getMostRecentBrowserWindow()); + 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 = getId(id); + return gDevTools.showToolbox(targetFor(tab), id); +}; +exports.openToolbox = openToolbox; + +const closeToolbox = tab => gDevTools.closeToolbox(targetFor(tab)); +exports.closeToolbox = closeToolbox; + +const getToolbox = tab => gDevTools.getToolbox(targetFor(tab)); +exports.getToolbox = getToolbox; + +const openToolboxPanel = (id, tab) => { + 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 new file mode 100644 index 000000000..6a68ed12d --- /dev/null +++ b/addon-sdk/source/lib/dev/volcan.js @@ -0,0 +1,3848 @@ +/* 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= 0 ? fields.constructor : + function() {}; + var ancestor = fields.extends || Object; + + var descriptor = names.reduce(function(descriptor, key) { + descriptor[key] = describe(fields, key); + return descriptor; + }, {}); + + var prototype = Object.create(ancestor.prototype, descriptor); + + constructor.prototype = prototype; + prototype.constructor = constructor; + + return constructor; +}; +exports.Class = Class; + +},{}],4:[function(_dereq_,module,exports){ +"use strict"; + +var Class = _dereq_("./class").Class; +var TypeSystem = _dereq_("./type-system").TypeSystem; +var values = _dereq_("./util").values; +var Promise = _dereq_("es6-promise").Promise; +var MessageEvent = _dereq_("./event").MessageEvent; + +var specification = _dereq_("./specification/core.json"); + +function recoverActorDescriptions(error) { + console.warn("Failed to fetch protocol specification (see reason below). " + + "Using a fallback protocal specification!", + error); + return _dereq_("./specification/protocol.json"); +} + +// Type to represent superviser actor relations to actors they supervise +// in terms of lifetime management. +var Supervisor = Class({ + constructor: function(id) { + this.id = id; + this.workers = []; + } +}); + +var Telemetry = Class({ + add: function(id, ms) { + console.log("telemetry::", id, ms) + } +}); + +// Consider making client a root actor. + +var Client = Class({ + constructor: function() { + this.root = null; + this.telemetry = new Telemetry(); + + this.setupConnection(); + this.setupLifeManagement(); + this.setupTypeSystem(); + }, + + setupConnection: function() { + this.requests = []; + }, + setupLifeManagement: function() { + this.cache = Object.create(null); + this.graph = Object.create(null); + this.get = this.get.bind(this); + this.release = this.release.bind(this); + }, + setupTypeSystem: function() { + this.typeSystem = new TypeSystem(this); + this.typeSystem.registerTypes(specification); + }, + + connect: function(port) { + var client = this; + return new Promise(function(resolve, reject) { + client.port = port; + port.onmessage = client.receive.bind(client); + client.onReady = resolve; + client.onFail = reject; + + port.start(); + }); + }, + send: function(packet) { + this.port.postMessage(packet); + }, + request: function(packet) { + var client = this; + return new Promise(function(resolve, reject) { + client.requests.push(packet.to, { resolve: resolve, reject: reject }); + client.send(packet); + }); + }, + + receive: function(event) { + var packet = event.data; + if (!this.root) { + if (packet.from !== "root") + throw Error("Initial packet must be from root"); + if (!("applicationType" in packet)) + throw Error("Initial packet must contain applicationType field"); + + this.root = this.typeSystem.read("root", null, "root"); + this.root + .protocolDescription() + .catch(recoverActorDescriptions) + .then(this.typeSystem.registerTypes.bind(this.typeSystem)) + .then(this.onReady.bind(this, this.root), this.onFail); + } else { + var actor = this.get(packet.from) || this.root; + var event = actor.events[packet.type]; + if (event) { + var message = new MessageEvent(packet.type, { + data: event.read(packet) + }); + actor.dispatchEvent(message); + } else { + var index = this.requests.indexOf(actor.id); + if (index >= 0) { + var request = this.requests.splice(index, 2).pop(); + if (packet.error) + request.reject(packet); + else + request.resolve(packet); + } else { + console.error(Error("Unexpected packet " + JSON.stringify(packet, 2, 2)), + packet, + this.requests.slice(0)); + } + } + } + }, + + get: function(id) { + return this.cache[id]; + }, + supervisorOf: function(actor) { + for (var id in this.graph) { + if (this.graph[id].indexOf(actor.id) >= 0) { + return id; + } + } + }, + workersOf: function(actor) { + return this.graph[actor.id]; + }, + supervise: function(actor, worker) { + var workers = this.workersOf(actor) + if (workers.indexOf(worker.id) < 0) { + workers.push(worker.id); + } + }, + unsupervise: function(actor, worker) { + var workers = this.workersOf(actor); + var index = workers.indexOf(worker.id) + if (index >= 0) { + workers.splice(index, 1) + } + }, + + register: function(actor) { + var registered = this.get(actor.id); + if (!registered) { + this.cache[actor.id] = actor; + this.graph[actor.id] = []; + } else if (registered !== actor) { + throw new Error("Different actor with same id is already registered"); + } + }, + unregister: function(actor) { + if (this.get(actor.id)) { + delete this.cache[actor.id]; + delete this.graph[actor.id]; + } + }, + + release: function(actor) { + var supervisor = this.supervisorOf(actor); + if (supervisor) + this.unsupervise(supervisor, actor); + + var workers = this.workersOf(actor) + + if (workers) { + workers.map(this.get).forEach(this.release) + } + this.unregister(actor); + } +}); +exports.Client = Client; + +},{"./class":3,"./event":5,"./specification/core.json":23,"./specification/protocol.json":24,"./type-system":25,"./util":26,"es6-promise":2}],5:[function(_dereq_,module,exports){ +"use strict"; + +var Symbol = _dereq_("es6-symbol") +var EventEmitter = _dereq_("events").EventEmitter; +var Class = _dereq_("./class").Class; + +var $bound = Symbol("EventTarget/handleEvent"); +var $emitter = Symbol("EventTarget/emitter"); + +function makeHandler(handler) { + return function(event) { + handler.handleEvent(event); + } +} + +var EventTarget = Class({ + constructor: function() { + Object.defineProperty(this, $emitter, { + enumerable: false, + configurable: true, + writable: true, + value: new EventEmitter() + }); + }, + addEventListener: function(type, handler) { + if (typeof(handler) === "function") { + this[$emitter].on(type, handler); + } + else if (handler && typeof(handler) === "object") { + if (!handler[$bound]) handler[$bound] = makeHandler(handler); + this[$emitter].on(type, handler[$bound]); + } + }, + removeEventListener: function(type, handler) { + if (typeof(handler) === "function") + this[$emitter].removeListener(type, handler); + else if (handler && handler[$bound]) + this[$emitter].removeListener(type, handler[$bound]); + }, + dispatchEvent: function(event) { + event.target = this; + this[$emitter].emit(event.type, event); + } +}); +exports.EventTarget = EventTarget; + +var MessageEvent = Class({ + constructor: function(type, options) { + options = options || {}; + this.type = type; + this.data = options.data === void(0) ? null : options.data; + + this.lastEventId = options.lastEventId || ""; + this.origin = options.origin || ""; + this.bubbles = options.bubbles || false; + this.cancelable = options.cancelable || false; + }, + source: null, + ports: null, + preventDefault: function() { + }, + stopPropagation: function() { + }, + stopImmediatePropagation: function() { + } +}); +exports.MessageEvent = MessageEvent; + +},{"./class":3,"es6-symbol":7,"events":6}],6:[function(_dereq_,module,exports){ +// 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. + +function EventEmitter() { + this._events = this._events || {}; + this._maxListeners = this._maxListeners || undefined; +} +module.exports = EventEmitter; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +EventEmitter.defaultMaxListeners = 10; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function(n) { + if (!isNumber(n) || n < 0 || isNaN(n)) + throw TypeError('n must be a positive number'); + this._maxListeners = n; + return this; +}; + +EventEmitter.prototype.emit = function(type) { + var er, handler, len, args, i, listeners; + + if (!this._events) + this._events = {}; + + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events.error || + (isObject(this._events.error) && !this._events.error.length)) { + er = arguments[1]; + if (er instanceof Error) { + throw er; // Unhandled 'error' event + } else { + throw TypeError('Uncaught, unspecified "error" event.'); + } + return false; + } + } + + handler = this._events[type]; + + if (isUndefined(handler)) + return false; + + if (isFunction(handler)) { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + handler.apply(this, args); + } + } else if (isObject(handler)) { + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + + listeners = handler.slice(); + len = listeners.length; + for (i = 0; i < len; i++) + listeners[i].apply(this, args); + } + + return true; +}; + +EventEmitter.prototype.addListener = function(type, listener) { + var m; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events) + this._events = {}; + + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (this._events.newListener) + this.emit('newListener', type, + isFunction(listener.listener) ? + listener.listener : listener); + + if (!this._events[type]) + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + else if (isObject(this._events[type])) + // If we've already got an array, just append. + this._events[type].push(listener); + else + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + + // Check for listener leak + if (isObject(this._events[type]) && !this._events[type].warned) { + var m; + if (!isUndefined(this._maxListeners)) { + m = this._maxListeners; + } else { + m = EventEmitter.defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + if (typeof console.trace === 'function') { + // not supported in IE 10 + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events || !this._events[type]) + return this; + + list = this._events[type]; + length = list.length; + position = -1; + + if (list === listener || + (isFunction(list.listener) && list.listener === listener)) { + delete this._events[type]; + if (this._events.removeListener) + this.emit('removeListener', type, listener); + + } else if (isObject(list)) { + for (i = length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + + if (this._events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + + if (!this._events) + return this; + + // not listening for removeListener, no need to emit + if (!this._events.removeListener) { + if (arguments.length === 0) + this._events = {}; + else if (this._events[type]) + delete this._events[type]; + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (key in this._events) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + return this; + } + + listeners = this._events[type]; + + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else { + // LIFO order + while (listeners.length) + this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) + ret = []; + else if (isFunction(this._events[type])) + ret = [this._events[type]]; + else + ret = this._events[type].slice(); + return ret; +}; + +EventEmitter.listenerCount = function(emitter, type) { + var ret; + if (!emitter._events || !emitter._events[type]) + ret = 0; + else if (isFunction(emitter._events[type])) + ret = 1; + else + ret = emitter._events[type].length; + return ret; +}; + +function isFunction(arg) { + return typeof arg === 'function'; +} + +function isNumber(arg) { + return typeof arg === 'number'; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isUndefined(arg) { + return arg === void 0; +} + +},{}],7:[function(_dereq_,module,exports){ +'use strict'; + +module.exports = _dereq_('./is-implemented')() ? Symbol : _dereq_('./polyfill'); + +},{"./is-implemented":8,"./polyfill":22}],8:[function(_dereq_,module,exports){ +'use strict'; + +module.exports = function () { + var symbol; + if (typeof Symbol !== 'function') return false; + symbol = Symbol('test symbol'); + try { + if (String(symbol) !== 'Symbol (test symbol)') return false; + } catch (e) { return false; } + if (typeof Symbol.iterator === 'symbol') return true; + + // Return 'true' for polyfills + if (typeof Symbol.isConcatSpreadable !== 'object') return false; + if (typeof Symbol.isRegExp !== 'object') return false; + if (typeof Symbol.iterator !== 'object') return false; + if (typeof Symbol.toPrimitive !== 'object') return false; + if (typeof Symbol.toStringTag !== 'object') return false; + if (typeof Symbol.unscopables !== 'object') return false; + + return true; +}; + +},{}],9:[function(_dereq_,module,exports){ +'use strict'; + +var assign = _dereq_('es5-ext/object/assign') + , normalizeOpts = _dereq_('es5-ext/object/normalize-options') + , isCallable = _dereq_('es5-ext/object/is-callable') + , contains = _dereq_('es5-ext/string/#/contains') + + , d; + +d = module.exports = function (dscr, value/*, options*/) { + var c, e, w, options, desc; + if ((arguments.length < 2) || (typeof dscr !== 'string')) { + options = value; + value = dscr; + dscr = null; + } else { + options = arguments[2]; + } + if (dscr == null) { + c = w = true; + e = false; + } else { + c = contains.call(dscr, 'c'); + e = contains.call(dscr, 'e'); + w = contains.call(dscr, 'w'); + } + + desc = { value: value, configurable: c, enumerable: e, writable: w }; + return !options ? desc : assign(normalizeOpts(options), desc); +}; + +d.gs = function (dscr, get, set/*, options*/) { + var c, e, options, desc; + if (typeof dscr !== 'string') { + options = set; + set = get; + get = dscr; + dscr = null; + } else { + options = arguments[3]; + } + if (get == null) { + get = undefined; + } else if (!isCallable(get)) { + options = get; + get = set = undefined; + } else if (set == null) { + set = undefined; + } else if (!isCallable(set)) { + options = set; + set = undefined; + } + if (dscr == null) { + c = true; + e = false; + } else { + c = contains.call(dscr, 'c'); + e = contains.call(dscr, 'e'); + } + + desc = { get: get, set: set, configurable: c, enumerable: e }; + return !options ? desc : assign(normalizeOpts(options), desc); +}; + +},{"es5-ext/object/assign":10,"es5-ext/object/is-callable":13,"es5-ext/object/normalize-options":17,"es5-ext/string/#/contains":19}],10:[function(_dereq_,module,exports){ +'use strict'; + +module.exports = _dereq_('./is-implemented')() + ? Object.assign + : _dereq_('./shim'); + +},{"./is-implemented":11,"./shim":12}],11:[function(_dereq_,module,exports){ +'use strict'; + +module.exports = function () { + var assign = Object.assign, obj; + if (typeof assign !== 'function') return false; + obj = { foo: 'raz' }; + assign(obj, { bar: 'dwa' }, { trzy: 'trzy' }); + return (obj.foo + obj.bar + obj.trzy) === 'razdwatrzy'; +}; + +},{}],12:[function(_dereq_,module,exports){ +'use strict'; + +var keys = _dereq_('../keys') + , value = _dereq_('../valid-value') + + , max = Math.max; + +module.exports = function (dest, src/*, …srcn*/) { + var error, i, l = max(arguments.length, 2), assign; + dest = Object(value(dest)); + assign = function (key) { + try { dest[key] = src[key]; } catch (e) { + if (!error) error = e; + } + }; + for (i = 1; i < l; ++i) { + src = arguments[i]; + keys(src).forEach(assign); + } + if (error !== undefined) throw error; + return dest; +}; + +},{"../keys":14,"../valid-value":18}],13:[function(_dereq_,module,exports){ +// Deprecated + +'use strict'; + +module.exports = function (obj) { return typeof obj === 'function'; }; + +},{}],14:[function(_dereq_,module,exports){ +'use strict'; + +module.exports = _dereq_('./is-implemented')() + ? Object.keys + : _dereq_('./shim'); + +},{"./is-implemented":15,"./shim":16}],15:[function(_dereq_,module,exports){ +'use strict'; + +module.exports = function () { + try { + Object.keys('primitive'); + return true; + } catch (e) { return false; } +}; + +},{}],16:[function(_dereq_,module,exports){ +'use strict'; + +var keys = Object.keys; + +module.exports = function (object) { + return keys(object == null ? object : Object(object)); +}; + +},{}],17:[function(_dereq_,module,exports){ +'use strict'; + +var assign = _dereq_('./assign') + + , forEach = Array.prototype.forEach + , create = Object.create, getPrototypeOf = Object.getPrototypeOf + + , process; + +process = function (src, obj) { + var proto = getPrototypeOf(src); + return assign(proto ? process(proto, obj) : obj, src); +}; + +module.exports = function (options/*, …options*/) { + var result = create(null); + forEach.call(arguments, function (options) { + if (options == null) return; + process(Object(options), result); + }); + return result; +}; + +},{"./assign":10}],18:[function(_dereq_,module,exports){ +'use strict'; + +module.exports = function (value) { + if (value == null) throw new TypeError("Cannot use null or undefined"); + return value; +}; + +},{}],19:[function(_dereq_,module,exports){ +'use strict'; + +module.exports = _dereq_('./is-implemented')() + ? String.prototype.contains + : _dereq_('./shim'); + +},{"./is-implemented":20,"./shim":21}],20:[function(_dereq_,module,exports){ +'use strict'; + +var str = 'razdwatrzy'; + +module.exports = function () { + if (typeof str.contains !== 'function') return false; + return ((str.contains('dwa') === true) && (str.contains('foo') === false)); +}; + +},{}],21:[function(_dereq_,module,exports){ +'use strict'; + +var indexOf = String.prototype.indexOf; + +module.exports = function (searchString/*, position*/) { + return indexOf.call(this, searchString, arguments[1]) > -1; +}; + +},{}],22:[function(_dereq_,module,exports){ +'use strict'; + +var d = _dereq_('d') + + , create = Object.create, defineProperties = Object.defineProperties + , generateName, Symbol; + +generateName = (function () { + var created = create(null); + return function (desc) { + var postfix = 0; + while (created[desc + (postfix || '')]) ++postfix; + desc += (postfix || ''); + created[desc] = true; + return '@@' + desc; + }; +}()); + +module.exports = Symbol = function (description) { + var symbol; + if (this instanceof Symbol) { + throw new TypeError('TypeError: Symbol is not a constructor'); + } + symbol = create(Symbol.prototype); + description = (description === undefined ? '' : String(description)); + return defineProperties(symbol, { + __description__: d('', description), + __name__: d('', generateName(description)) + }); +}; + +Object.defineProperties(Symbol, { + create: d('', Symbol('create')), + hasInstance: d('', Symbol('hasInstance')), + isConcatSpreadable: d('', Symbol('isConcatSpreadable')), + isRegExp: d('', Symbol('isRegExp')), + iterator: d('', Symbol('iterator')), + toPrimitive: d('', Symbol('toPrimitive')), + toStringTag: d('', Symbol('toStringTag')), + unscopables: d('', Symbol('unscopables')) +}); + +defineProperties(Symbol.prototype, { + properToString: d(function () { + return 'Symbol (' + this.__description__ + ')'; + }), + toString: d('', function () { return this.__name__; }) +}); +Object.defineProperty(Symbol.prototype, Symbol.toPrimitive, d('', + function (hint) { + throw new TypeError("Conversion of symbol objects is not allowed"); + })); +Object.defineProperty(Symbol.prototype, Symbol.toStringTag, d('c', 'Symbol')); + +},{"d":9}],23:[function(_dereq_,module,exports){ +module.exports={ + "types": { + "root": { + "category": "actor", + "typeName": "root", + "methods": [ + { + "name": "echo", + "request": { + "string": { "_arg": 0, "type": "string" } + }, + "response": { + "string": { "_retval": "string" } + } + }, + { + "name": "listTabs", + "request": {}, + "response": { "_retval": "tablist" } + }, + { + "name": "protocolDescription", + "request": {}, + "response": { "_retval": "json" } + } + ], + "events": { + "tabListChanged": {} + } + }, + "tablist": { + "category": "dict", + "typeName": "tablist", + "specializations": { + "selected": "number", + "tabs": "array:tab", + "url": "string", + "consoleActor": "console", + "inspectorActor": "inspector", + "styleSheetsActor": "stylesheets", + "styleEditorActor": "styleeditor", + "memoryActor": "memory", + "eventLoopLagActor": "eventLoopLag", + "preferenceActor": "preference", + "deviceActor": "device", + + "profilerActor": "profiler", + "chromeDebugger": "chromeDebugger", + "webappsActor": "webapps" + } + }, + "tab": { + "category": "actor", + "typeName": "tab", + "fields": { + "title": "string", + "url": "string", + "outerWindowID": "number", + "inspectorActor": "inspector", + "callWatcherActor": "call-watcher", + "canvasActor": "canvas", + "webglActor": "webgl", + "webaudioActor": "webaudio", + "storageActor": "storage", + "gcliActor": "gcli", + "memoryActor": "memory", + "eventLoopLag": "eventLoopLag", + "styleSheetsActor": "stylesheets", + "styleEditorActor": "styleeditor", + + "consoleActor": "console", + "traceActor": "trace" + }, + "methods": [ + { + "name": "attach", + "request": {}, + "response": { "_retval": "json" } + } + ], + "events": { + "tabNavigated": { + "typeName": "tabNavigated" + } + } + }, + "console": { + "category": "actor", + "typeName": "console", + "methods": [ + { + "name": "evaluateJS", + "request": { + "text": { + "_option": 0, + "type": "string" + }, + "url": { + "_option": 1, + "type": "string" + }, + "bindObjectActor": { + "_option": 2, + "type": "nullable:string" + }, + "frameActor": { + "_option": 2, + "type": "nullable:string" + }, + "selectedNodeActor": { + "_option": 2, + "type": "nullable:string" + } + }, + "response": { + "_retval": "evaluatejsresponse" + } + } + ], + "events": {} + }, + "evaluatejsresponse": { + "category": "dict", + "typeName": "evaluatejsresponse", + "specializations": { + "result": "object", + "exception": "object", + "exceptionMessage": "string", + "input": "string" + } + }, + "object": { + "category": "actor", + "typeName": "object", + "methods": [ + { + "name": "property", + "request": { + "name": { + "_arg": 0, + "type": "string" + } + }, + "response": { + "descriptor": { + "_retval": "json" + } + } + } + ] + } + } +} + +},{}],24:[function(_dereq_,module,exports){ +module.exports={ + "types": { + "longstractor": { + "category": "actor", + "typeName": "longstractor", + "methods": [ + { + "name": "substring", + "request": { + "type": "substring", + "start": { + "_arg": 0, + "type": "primitive" + }, + "end": { + "_arg": 1, + "type": "primitive" + } + }, + "response": { + "substring": { + "_retval": "primitive" + } + } + }, + { + "name": "release", + "release": true, + "request": { + "type": "release" + }, + "response": {} + } + ], + "events": {} + }, + "stylesheet": { + "category": "actor", + "typeName": "stylesheet", + "methods": [ + { + "name": "toggleDisabled", + "request": { + "type": "toggleDisabled" + }, + "response": { + "disabled": { + "_retval": "boolean" + } + } + }, + { + "name": "getText", + "request": { + "type": "getText" + }, + "response": { + "text": { + "_retval": "longstring" + } + } + }, + { + "name": "getOriginalSources", + "request": { + "type": "getOriginalSources" + }, + "response": { + "originalSources": { + "_retval": "nullable:array:originalsource" + } + } + }, + { + "name": "getOriginalLocation", + "request": { + "type": "getOriginalLocation", + "line": { + "_arg": 0, + "type": "number" + }, + "column": { + "_arg": 1, + "type": "number" + } + }, + "response": { + "_retval": "originallocationresponse" + } + }, + { + "name": "update", + "request": { + "type": "update", + "text": { + "_arg": 0, + "type": "string" + }, + "transition": { + "_arg": 1, + "type": "boolean" + } + }, + "response": {} + } + ], + "events": { + "property-change": { + "type": "propertyChange", + "property": { + "_arg": 0, + "type": "string" + }, + "value": { + "_arg": 1, + "type": "json" + } + }, + "style-applied": { + "type": "styleApplied" + } + } + }, + "originalsource": { + "category": "actor", + "typeName": "originalsource", + "methods": [ + { + "name": "getText", + "request": { + "type": "getText" + }, + "response": { + "text": { + "_retval": "longstring" + } + } + } + ], + "events": {} + }, + "stylesheets": { + "category": "actor", + "typeName": "stylesheets", + "methods": [ + { + "name": "getStyleSheets", + "request": { + "type": "getStyleSheets" + }, + "response": { + "styleSheets": { + "_retval": "array:stylesheet" + } + } + }, + { + "name": "addStyleSheet", + "request": { + "type": "addStyleSheet", + "text": { + "_arg": 0, + "type": "string" + } + }, + "response": { + "styleSheet": { + "_retval": "stylesheet" + } + } + } + ], + "events": {} + }, + "originallocationresponse": { + "category": "dict", + "typeName": "originallocationresponse", + "specializations": { + "source": "string", + "line": "number", + "column": "number" + } + }, + "domnode": { + "category": "actor", + "typeName": "domnode", + "methods": [ + { + "name": "getNodeValue", + "request": { + "type": "getNodeValue" + }, + "response": { + "value": { + "_retval": "longstring" + } + } + }, + { + "name": "setNodeValue", + "request": { + "type": "setNodeValue", + "value": { + "_arg": 0, + "type": "primitive" + } + }, + "response": {} + }, + { + "name": "getImageData", + "request": { + "type": "getImageData", + "maxDim": { + "_arg": 0, + "type": "nullable:number" + } + }, + "response": { + "_retval": "imageData" + } + }, + { + "name": "modifyAttributes", + "request": { + "type": "modifyAttributes", + "modifications": { + "_arg": 0, + "type": "array:json" + } + }, + "response": {} + } + ], + "events": {} + }, + "appliedstyle": { + "category": "dict", + "typeName": "appliedstyle", + "specializations": { + "rule": "domstylerule#actorid", + "inherited": "nullable:domnode#actorid" + } + }, + "matchedselector": { + "category": "dict", + "typeName": "matchedselector", + "specializations": { + "rule": "domstylerule#actorid", + "selector": "string", + "value": "string", + "status": "number" + } + }, + "matchedselectorresponse": { + "category": "dict", + "typeName": "matchedselectorresponse", + "specializations": { + "rules": "array:domstylerule", + "sheets": "array:stylesheet", + "matched": "array:matchedselector" + } + }, + "appliedStylesReturn": { + "category": "dict", + "typeName": "appliedStylesReturn", + "specializations": { + "entries": "array:appliedstyle", + "rules": "array:domstylerule", + "sheets": "array:stylesheet" + } + }, + "pagestyle": { + "category": "actor", + "typeName": "pagestyle", + "methods": [ + { + "name": "getComputed", + "request": { + "type": "getComputed", + "node": { + "_arg": 0, + "type": "domnode" + }, + "markMatched": { + "_option": 1, + "type": "boolean" + }, + "onlyMatched": { + "_option": 1, + "type": "boolean" + }, + "filter": { + "_option": 1, + "type": "string" + } + }, + "response": { + "computed": { + "_retval": "json" + } + } + }, + { + "name": "getMatchedSelectors", + "request": { + "type": "getMatchedSelectors", + "node": { + "_arg": 0, + "type": "domnode" + }, + "property": { + "_arg": 1, + "type": "string" + }, + "filter": { + "_option": 2, + "type": "string" + } + }, + "response": { + "_retval": "matchedselectorresponse" + } + }, + { + "name": "getApplied", + "request": { + "type": "getApplied", + "node": { + "_arg": 0, + "type": "domnode" + }, + "inherited": { + "_option": 1, + "type": "boolean" + }, + "matchedSelectors": { + "_option": 1, + "type": "boolean" + }, + "filter": { + "_option": 1, + "type": "string" + } + }, + "response": { + "_retval": "appliedStylesReturn" + } + }, + { + "name": "getLayout", + "request": { + "type": "getLayout", + "node": { + "_arg": 0, + "type": "domnode" + }, + "autoMargins": { + "_option": 1, + "type": "boolean" + } + }, + "response": { + "_retval": "json" + } + } + ], + "events": {} + }, + "domstylerule": { + "category": "actor", + "typeName": "domstylerule", + "methods": [ + { + "name": "modifyProperties", + "request": { + "type": "modifyProperties", + "modifications": { + "_arg": 0, + "type": "array:json" + } + }, + "response": { + "rule": { + "_retval": "domstylerule" + } + } + } + ], + "events": {} + }, + "highlighter": { + "category": "actor", + "typeName": "highlighter", + "methods": [ + { + "name": "showBoxModel", + "request": { + "type": "showBoxModel", + "node": { + "_arg": 0, + "type": "domnode" + }, + "region": { + "_option": 1, + "type": "primitive" + } + }, + "response": {} + }, + { + "name": "hideBoxModel", + "request": { + "type": "hideBoxModel" + }, + "response": {} + }, + { + "name": "pick", + "request": { + "type": "pick" + }, + "response": {} + }, + { + "name": "cancelPick", + "request": { + "type": "cancelPick" + }, + "response": {} + } + ], + "events": {} + }, + "imageData": { + "category": "dict", + "typeName": "imageData", + "specializations": { + "data": "nullable:longstring", + "size": "json" + } + }, + "disconnectedNode": { + "category": "dict", + "typeName": "disconnectedNode", + "specializations": { + "node": "domnode", + "newParents": "array:domnode" + } + }, + "disconnectedNodeArray": { + "category": "dict", + "typeName": "disconnectedNodeArray", + "specializations": { + "nodes": "array:domnode", + "newParents": "array:domnode" + } + }, + "dommutation": { + "category": "dict", + "typeName": "dommutation", + "specializations": {} + }, + "domnodelist": { + "category": "actor", + "typeName": "domnodelist", + "methods": [ + { + "name": "item", + "request": { + "type": "item", + "item": { + "_arg": 0, + "type": "primitive" + } + }, + "response": { + "_retval": "disconnectedNode" + } + }, + { + "name": "items", + "request": { + "type": "items", + "start": { + "_arg": 0, + "type": "nullable:number" + }, + "end": { + "_arg": 1, + "type": "nullable:number" + } + }, + "response": { + "_retval": "disconnectedNodeArray" + } + }, + { + "name": "release", + "release": true, + "request": { + "type": "release" + }, + "response": {} + } + ], + "events": {} + }, + "domtraversalarray": { + "category": "dict", + "typeName": "domtraversalarray", + "specializations": { + "nodes": "array:domnode" + } + }, + "domwalker": { + "category": "actor", + "typeName": "domwalker", + "methods": [ + { + "name": "release", + "release": true, + "request": { + "type": "release" + }, + "response": {} + }, + { + "name": "pick", + "request": { + "type": "pick" + }, + "response": { + "_retval": "disconnectedNode" + } + }, + { + "name": "cancelPick", + "request": { + "type": "cancelPick" + }, + "response": {} + }, + { + "name": "highlight", + "request": { + "type": "highlight", + "node": { + "_arg": 0, + "type": "nullable:domnode" + } + }, + "response": {} + }, + { + "name": "document", + "request": { + "type": "document", + "node": { + "_arg": 0, + "type": "nullable:domnode" + } + }, + "response": { + "node": { + "_retval": "domnode" + } + } + }, + { + "name": "documentElement", + "request": { + "type": "documentElement", + "node": { + "_arg": 0, + "type": "nullable:domnode" + } + }, + "response": { + "node": { + "_retval": "domnode" + } + } + }, + { + "name": "parents", + "request": { + "type": "parents", + "node": { + "_arg": 0, + "type": "domnode" + }, + "sameDocument": { + "_option": 1, + "type": "primitive" + } + }, + "response": { + "nodes": { + "_retval": "array:domnode" + } + } + }, + { + "name": "retainNode", + "request": { + "type": "retainNode", + "node": { + "_arg": 0, + "type": "domnode" + } + }, + "response": {} + }, + { + "name": "unretainNode", + "request": { + "type": "unretainNode", + "node": { + "_arg": 0, + "type": "domnode" + } + }, + "response": {} + }, + { + "name": "releaseNode", + "request": { + "type": "releaseNode", + "node": { + "_arg": 0, + "type": "domnode" + }, + "force": { + "_option": 1, + "type": "primitive" + } + }, + "response": {} + }, + { + "name": "children", + "request": { + "type": "children", + "node": { + "_arg": 0, + "type": "domnode" + }, + "maxNodes": { + "_option": 1, + "type": "primitive" + }, + "center": { + "_option": 1, + "type": "domnode" + }, + "start": { + "_option": 1, + "type": "domnode" + }, + "whatToShow": { + "_option": 1, + "type": "primitive" + } + }, + "response": { + "_retval": "domtraversalarray" + } + }, + { + "name": "siblings", + "request": { + "type": "siblings", + "node": { + "_arg": 0, + "type": "domnode" + }, + "maxNodes": { + "_option": 1, + "type": "primitive" + }, + "center": { + "_option": 1, + "type": "domnode" + }, + "start": { + "_option": 1, + "type": "domnode" + }, + "whatToShow": { + "_option": 1, + "type": "primitive" + } + }, + "response": { + "_retval": "domtraversalarray" + } + }, + { + "name": "nextSibling", + "request": { + "type": "nextSibling", + "node": { + "_arg": 0, + "type": "domnode" + }, + "whatToShow": { + "_option": 1, + "type": "primitive" + } + }, + "response": { + "node": { + "_retval": "nullable:domnode" + } + } + }, + { + "name": "previousSibling", + "request": { + "type": "previousSibling", + "node": { + "_arg": 0, + "type": "domnode" + }, + "whatToShow": { + "_option": 1, + "type": "primitive" + } + }, + "response": { + "node": { + "_retval": "nullable:domnode" + } + } + }, + { + "name": "querySelector", + "request": { + "type": "querySelector", + "node": { + "_arg": 0, + "type": "domnode" + }, + "selector": { + "_arg": 1, + "type": "primitive" + } + }, + "response": { + "_retval": "disconnectedNode" + } + }, + { + "name": "querySelectorAll", + "request": { + "type": "querySelectorAll", + "node": { + "_arg": 0, + "type": "domnode" + }, + "selector": { + "_arg": 1, + "type": "primitive" + } + }, + "response": { + "list": { + "_retval": "domnodelist" + } + } + }, + { + "name": "getSuggestionsForQuery", + "request": { + "type": "getSuggestionsForQuery", + "query": { + "_arg": 0, + "type": "primitive" + }, + "completing": { + "_arg": 1, + "type": "primitive" + }, + "selectorState": { + "_arg": 2, + "type": "primitive" + } + }, + "response": { + "list": { + "_retval": "array:array:string" + } + } + }, + { + "name": "addPseudoClassLock", + "request": { + "type": "addPseudoClassLock", + "node": { + "_arg": 0, + "type": "domnode" + }, + "pseudoClass": { + "_arg": 1, + "type": "primitive" + }, + "parents": { + "_option": 2, + "type": "primitive" + } + }, + "response": {} + }, + { + "name": "hideNode", + "request": { + "type": "hideNode", + "node": { + "_arg": 0, + "type": "domnode" + } + }, + "response": {} + }, + { + "name": "unhideNode", + "request": { + "type": "unhideNode", + "node": { + "_arg": 0, + "type": "domnode" + } + }, + "response": {} + }, + { + "name": "removePseudoClassLock", + "request": { + "type": "removePseudoClassLock", + "node": { + "_arg": 0, + "type": "domnode" + }, + "pseudoClass": { + "_arg": 1, + "type": "primitive" + }, + "parents": { + "_option": 2, + "type": "primitive" + } + }, + "response": {} + }, + { + "name": "clearPseudoClassLocks", + "request": { + "type": "clearPseudoClassLocks", + "node": { + "_arg": 0, + "type": "nullable:domnode" + } + }, + "response": {} + }, + { + "name": "innerHTML", + "request": { + "type": "innerHTML", + "node": { + "_arg": 0, + "type": "domnode" + } + }, + "response": { + "value": { + "_retval": "longstring" + } + } + }, + { + "name": "outerHTML", + "request": { + "type": "outerHTML", + "node": { + "_arg": 0, + "type": "domnode" + } + }, + "response": { + "value": { + "_retval": "longstring" + } + } + }, + { + "name": "setOuterHTML", + "request": { + "type": "setOuterHTML", + "node": { + "_arg": 0, + "type": "domnode" + }, + "value": { + "_arg": 1, + "type": "primitive" + } + }, + "response": {} + }, + { + "name": "removeNode", + "request": { + "type": "removeNode", + "node": { + "_arg": 0, + "type": "domnode" + } + }, + "response": { + "nextSibling": { + "_retval": "nullable:domnode" + } + } + }, + { + "name": "insertBefore", + "request": { + "type": "insertBefore", + "node": { + "_arg": 0, + "type": "domnode" + }, + "parent": { + "_arg": 1, + "type": "domnode" + }, + "sibling": { + "_arg": 2, + "type": "nullable:domnode" + } + }, + "response": {} + }, + { + "name": "getMutations", + "request": { + "type": "getMutations", + "cleanup": { + "_option": 0, + "type": "primitive" + } + }, + "response": { + "mutations": { + "_retval": "array:dommutation" + } + } + }, + { + "name": "isInDOMTree", + "request": { + "type": "isInDOMTree", + "node": { + "_arg": 0, + "type": "domnode" + } + }, + "response": { + "attached": { + "_retval": "boolean" + } + } + }, + { + "name": "getNodeActorFromObjectActor", + "request": { + "type": "getNodeActorFromObjectActor", + "objectActorID": { + "_arg": 0, + "type": "string" + } + }, + "response": { + "nodeFront": { + "_retval": "nullable:disconnectedNode" + } + } + } + ], + "events": { + "new-mutations": { + "type": "newMutations" + }, + "picker-node-picked": { + "type": "pickerNodePicked", + "node": { + "_arg": 0, + "type": "disconnectedNode" + } + }, + "picker-node-hovered": { + "type": "pickerNodeHovered", + "node": { + "_arg": 0, + "type": "disconnectedNode" + } + }, + "highlighter-ready": { + "type": "highlighter-ready" + }, + "highlighter-hide": { + "type": "highlighter-hide" + } + } + }, + "inspector": { + "category": "actor", + "typeName": "inspector", + "methods": [ + { + "name": "getWalker", + "request": { + "type": "getWalker" + }, + "response": { + "walker": { + "_retval": "domwalker" + } + } + }, + { + "name": "getPageStyle", + "request": { + "type": "getPageStyle" + }, + "response": { + "pageStyle": { + "_retval": "pagestyle" + } + } + }, + { + "name": "getHighlighter", + "request": { + "type": "getHighlighter", + "autohide": { + "_arg": 0, + "type": "boolean" + } + }, + "response": { + "highligter": { + "_retval": "highlighter" + } + } + }, + { + "name": "getImageDataFromURL", + "request": { + "type": "getImageDataFromURL", + "url": { + "_arg": 0, + "type": "primitive" + }, + "maxDim": { + "_arg": 1, + "type": "nullable:number" + } + }, + "response": { + "_retval": "imageData" + } + } + ], + "events": {} + }, + "call-stack-item": { + "category": "dict", + "typeName": "call-stack-item", + "specializations": { + "name": "string", + "file": "string", + "line": "number" + } + }, + "call-details": { + "category": "dict", + "typeName": "call-details", + "specializations": { + "type": "number", + "name": "string", + "stack": "array:call-stack-item" + } + }, + "function-call": { + "category": "actor", + "typeName": "function-call", + "methods": [ + { + "name": "getDetails", + "request": { + "type": "getDetails" + }, + "response": { + "info": { + "_retval": "call-details" + } + } + } + ], + "events": {} + }, + "call-watcher": { + "category": "actor", + "typeName": "call-watcher", + "methods": [ + { + "name": "setup", + "oneway": true, + "request": { + "type": "setup", + "tracedGlobals": { + "_option": 0, + "type": "nullable:array:string" + }, + "tracedFunctions": { + "_option": 0, + "type": "nullable:array:string" + }, + "startRecording": { + "_option": 0, + "type": "boolean" + }, + "performReload": { + "_option": 0, + "type": "boolean" + } + }, + "response": {} + }, + { + "name": "finalize", + "oneway": true, + "request": { + "type": "finalize" + }, + "response": {} + }, + { + "name": "isRecording", + "request": { + "type": "isRecording" + }, + "response": { + "_retval": "boolean" + } + }, + { + "name": "resumeRecording", + "request": { + "type": "resumeRecording" + }, + "response": {} + }, + { + "name": "pauseRecording", + "request": { + "type": "pauseRecording" + }, + "response": { + "calls": { + "_retval": "array:function-call" + } + } + }, + { + "name": "eraseRecording", + "request": { + "type": "eraseRecording" + }, + "response": {} + } + ], + "events": {} + }, + "snapshot-image": { + "category": "dict", + "typeName": "snapshot-image", + "specializations": { + "index": "number", + "width": "number", + "height": "number", + "flipped": "boolean", + "pixels": "uint32-array" + } + }, + "snapshot-overview": { + "category": "dict", + "typeName": "snapshot-overview", + "specializations": { + "calls": "array:function-call", + "thumbnails": "array:snapshot-image", + "screenshot": "snapshot-image" + } + }, + "frame-snapshot": { + "category": "actor", + "typeName": "frame-snapshot", + "methods": [ + { + "name": "getOverview", + "request": { + "type": "getOverview" + }, + "response": { + "overview": { + "_retval": "snapshot-overview" + } + } + }, + { + "name": "generateScreenshotFor", + "request": { + "type": "generateScreenshotFor", + "call": { + "_arg": 0, + "type": "function-call" + } + }, + "response": { + "screenshot": { + "_retval": "snapshot-image" + } + } + } + ], + "events": {} + }, + "canvas": { + "category": "actor", + "typeName": "canvas", + "methods": [ + { + "name": "setup", + "oneway": true, + "request": { + "type": "setup", + "reload": { + "_option": 0, + "type": "boolean" + } + }, + "response": {} + }, + { + "name": "finalize", + "oneway": true, + "request": { + "type": "finalize" + }, + "response": {} + }, + { + "name": "isInitialized", + "request": { + "type": "isInitialized" + }, + "response": { + "initialized": { + "_retval": "boolean" + } + } + }, + { + "name": "recordAnimationFrame", + "request": { + "type": "recordAnimationFrame" + }, + "response": { + "snapshot": { + "_retval": "frame-snapshot" + } + } + } + ], + "events": {} + }, + "gl-shader": { + "category": "actor", + "typeName": "gl-shader", + "methods": [ + { + "name": "getText", + "request": { + "type": "getText" + }, + "response": { + "text": { + "_retval": "string" + } + } + }, + { + "name": "compile", + "request": { + "type": "compile", + "text": { + "_arg": 0, + "type": "string" + } + }, + "response": { + "error": { + "_retval": "nullable:json" + } + } + } + ], + "events": {} + }, + "gl-program": { + "category": "actor", + "typeName": "gl-program", + "methods": [ + { + "name": "getVertexShader", + "request": { + "type": "getVertexShader" + }, + "response": { + "shader": { + "_retval": "gl-shader" + } + } + }, + { + "name": "getFragmentShader", + "request": { + "type": "getFragmentShader" + }, + "response": { + "shader": { + "_retval": "gl-shader" + } + } + }, + { + "name": "highlight", + "oneway": true, + "request": { + "type": "highlight", + "tint": { + "_arg": 0, + "type": "array:number" + } + }, + "response": {} + }, + { + "name": "unhighlight", + "oneway": true, + "request": { + "type": "unhighlight" + }, + "response": {} + }, + { + "name": "blackbox", + "oneway": true, + "request": { + "type": "blackbox" + }, + "response": {} + }, + { + "name": "unblackbox", + "oneway": true, + "request": { + "type": "unblackbox" + }, + "response": {} + } + ], + "events": {} + }, + "webgl": { + "category": "actor", + "typeName": "webgl", + "methods": [ + { + "name": "setup", + "oneway": true, + "request": { + "type": "setup", + "reload": { + "_option": 0, + "type": "boolean" + } + }, + "response": {} + }, + { + "name": "finalize", + "oneway": true, + "request": { + "type": "finalize" + }, + "response": {} + }, + { + "name": "getPrograms", + "request": { + "type": "getPrograms" + }, + "response": { + "programs": { + "_retval": "array:gl-program" + } + } + } + ], + "events": { + "program-linked": { + "type": "programLinked", + "program": { + "_arg": 0, + "type": "gl-program" + } + } + } + }, + "audionode": { + "category": "actor", + "typeName": "audionode", + "methods": [ + { + "name": "getType", + "request": { + "type": "getType" + }, + "response": { + "type": { + "_retval": "string" + } + } + }, + { + "name": "isSource", + "request": { + "type": "isSource" + }, + "response": { + "source": { + "_retval": "boolean" + } + } + }, + { + "name": "setParam", + "request": { + "type": "setParam", + "param": { + "_arg": 0, + "type": "string" + }, + "value": { + "_arg": 1, + "type": "nullable:primitive" + } + }, + "response": { + "error": { + "_retval": "nullable:json" + } + } + }, + { + "name": "getParam", + "request": { + "type": "getParam", + "param": { + "_arg": 0, + "type": "string" + } + }, + "response": { + "text": { + "_retval": "nullable:primitive" + } + } + }, + { + "name": "getParamFlags", + "request": { + "type": "getParamFlags", + "param": { + "_arg": 0, + "type": "string" + } + }, + "response": { + "flags": { + "_retval": "nullable:primitive" + } + } + }, + { + "name": "getParams", + "request": { + "type": "getParams" + }, + "response": { + "params": { + "_retval": "json" + } + } + } + ], + "events": {} + }, + "webaudio": { + "category": "actor", + "typeName": "webaudio", + "methods": [ + { + "name": "setup", + "oneway": true, + "request": { + "type": "setup", + "reload": { + "_option": 0, + "type": "boolean" + } + }, + "response": {} + }, + { + "name": "finalize", + "oneway": true, + "request": { + "type": "finalize" + }, + "response": {} + } + ], + "events": { + "start-context": { + "type": "startContext" + }, + "connect-node": { + "type": "connectNode", + "source": { + "_option": 0, + "type": "audionode" + }, + "dest": { + "_option": 0, + "type": "audionode" + } + }, + "disconnect-node": { + "type": "disconnectNode", + "source": { + "_arg": 0, + "type": "audionode" + } + }, + "connect-param": { + "type": "connectParam", + "source": { + "_arg": 0, + "type": "audionode" + }, + "param": { + "_arg": 1, + "type": "string" + } + }, + "change-param": { + "type": "changeParam", + "source": { + "_option": 0, + "type": "audionode" + }, + "param": { + "_option": 0, + "type": "string" + }, + "value": { + "_option": 0, + "type": "string" + } + }, + "create-node": { + "type": "createNode", + "source": { + "_arg": 0, + "type": "audionode" + } + } + } + }, + "old-stylesheet": { + "category": "actor", + "typeName": "old-stylesheet", + "methods": [ + { + "name": "toggleDisabled", + "request": { + "type": "toggleDisabled" + }, + "response": { + "disabled": { + "_retval": "boolean" + } + } + }, + { + "name": "fetchSource", + "request": { + "type": "fetchSource" + }, + "response": {} + }, + { + "name": "update", + "request": { + "type": "update", + "text": { + "_arg": 0, + "type": "string" + }, + "transition": { + "_arg": 1, + "type": "boolean" + } + }, + "response": {} + } + ], + "events": { + "property-change": { + "type": "propertyChange", + "property": { + "_arg": 0, + "type": "string" + }, + "value": { + "_arg": 1, + "type": "json" + } + }, + "source-load": { + "type": "sourceLoad", + "source": { + "_arg": 0, + "type": "string" + } + }, + "style-applied": { + "type": "styleApplied" + } + } + }, + "styleeditor": { + "category": "actor", + "typeName": "styleeditor", + "methods": [ + { + "name": "newDocument", + "request": { + "type": "newDocument" + }, + "response": {} + }, + { + "name": "newStyleSheet", + "request": { + "type": "newStyleSheet", + "text": { + "_arg": 0, + "type": "string" + } + }, + "response": { + "styleSheet": { + "_retval": "old-stylesheet" + } + } + } + ], + "events": { + "document-load": { + "type": "documentLoad", + "styleSheets": { + "_arg": 0, + "type": "array:old-stylesheet" + } + } + } + }, + "cookieobject": { + "category": "dict", + "typeName": "cookieobject", + "specializations": { + "name": "string", + "value": "longstring", + "path": "nullable:string", + "host": "string", + "isDomain": "boolean", + "isSecure": "boolean", + "isHttpOnly": "boolean", + "creationTime": "number", + "lastAccessed": "number", + "expires": "number" + } + }, + "cookiestoreobject": { + "category": "dict", + "typeName": "cookiestoreobject", + "specializations": { + "total": "number", + "offset": "number", + "data": "array:nullable:cookieobject" + } + }, + "storageobject": { + "category": "dict", + "typeName": "storageobject", + "specializations": { + "name": "string", + "value": "longstring" + } + }, + "storagestoreobject": { + "category": "dict", + "typeName": "storagestoreobject", + "specializations": { + "total": "number", + "offset": "number", + "data": "array:nullable:storageobject" + } + }, + "idbobject": { + "category": "dict", + "typeName": "idbobject", + "specializations": { + "name": "nullable:string", + "db": "nullable:string", + "objectStore": "nullable:string", + "origin": "nullable:string", + "version": "nullable:number", + "objectStores": "nullable:number", + "keyPath": "nullable:string", + "autoIncrement": "nullable:boolean", + "indexes": "nullable:string", + "value": "nullable:longstring" + } + }, + "idbstoreobject": { + "category": "dict", + "typeName": "idbstoreobject", + "specializations": { + "total": "number", + "offset": "number", + "data": "array:nullable:idbobject" + } + }, + "storeUpdateObject": { + "category": "dict", + "typeName": "storeUpdateObject", + "specializations": { + "changed": "nullable:json", + "deleted": "nullable:json", + "added": "nullable:json" + } + }, + "cookies": { + "category": "actor", + "typeName": "cookies", + "methods": [ + { + "name": "getStoreObjects", + "request": { + "type": "getStoreObjects", + "host": { + "_arg": 0, + "type": "primitive" + }, + "names": { + "_arg": 1, + "type": "nullable:array:string" + }, + "options": { + "_arg": 2, + "type": "nullable:json" + } + }, + "response": { + "_retval": "cookiestoreobject" + } + } + ], + "events": {} + }, + "localStorage": { + "category": "actor", + "typeName": "localStorage", + "methods": [ + { + "name": "getStoreObjects", + "request": { + "type": "getStoreObjects", + "host": { + "_arg": 0, + "type": "primitive" + }, + "names": { + "_arg": 1, + "type": "nullable:array:string" + }, + "options": { + "_arg": 2, + "type": "nullable:json" + } + }, + "response": { + "_retval": "storagestoreobject" + } + } + ], + "events": {} + }, + "sessionStorage": { + "category": "actor", + "typeName": "sessionStorage", + "methods": [ + { + "name": "getStoreObjects", + "request": { + "type": "getStoreObjects", + "host": { + "_arg": 0, + "type": "primitive" + }, + "names": { + "_arg": 1, + "type": "nullable:array:string" + }, + "options": { + "_arg": 2, + "type": "nullable:json" + } + }, + "response": { + "_retval": "storagestoreobject" + } + } + ], + "events": {} + }, + "indexedDB": { + "category": "actor", + "typeName": "indexedDB", + "methods": [ + { + "name": "getStoreObjects", + "request": { + "type": "getStoreObjects", + "host": { + "_arg": 0, + "type": "primitive" + }, + "names": { + "_arg": 1, + "type": "nullable:array:string" + }, + "options": { + "_arg": 2, + "type": "nullable:json" + } + }, + "response": { + "_retval": "idbstoreobject" + } + } + ], + "events": {} + }, + "storelist": { + "category": "dict", + "typeName": "storelist", + "specializations": { + "cookies": "cookies", + "localStorage": "localStorage", + "sessionStorage": "sessionStorage", + "indexedDB": "indexedDB" + } + }, + "storage": { + "category": "actor", + "typeName": "storage", + "methods": [ + { + "name": "listStores", + "request": { + "type": "listStores" + }, + "response": { + "_retval": "storelist" + } + } + ], + "events": { + "stores-update": { + "type": "storesUpdate", + "data": { + "_arg": 0, + "type": "storeUpdateObject" + } + }, + "stores-cleared": { + "type": "storesCleared", + "data": { + "_arg": 0, + "type": "json" + } + }, + "stores-reloaded": { + "type": "storesRelaoded", + "data": { + "_arg": 0, + "type": "json" + } + } + } + }, + "gcli": { + "category": "actor", + "typeName": "gcli", + "methods": [ + { + "name": "specs", + "request": { + "type": "specs" + }, + "response": { + "_retval": "json" + } + }, + { + "name": "execute", + "request": { + "type": "execute", + "typed": { + "_arg": 0, + "type": "string" + } + }, + "response": { + "_retval": "json" + } + }, + { + "name": "state", + "request": { + "type": "state", + "typed": { + "_arg": 0, + "type": "string" + }, + "start": { + "_arg": 1, + "type": "number" + }, + "rank": { + "_arg": 2, + "type": "number" + } + }, + "response": { + "_retval": "json" + } + }, + { + "name": "typeparse", + "request": { + "type": "typeparse", + "typed": { + "_arg": 0, + "type": "string" + }, + "param": { + "_arg": 1, + "type": "string" + } + }, + "response": { + "_retval": "json" + } + }, + { + "name": "typeincrement", + "request": { + "type": "typeincrement", + "typed": { + "_arg": 0, + "type": "string" + }, + "param": { + "_arg": 1, + "type": "string" + } + }, + "response": { + "_retval": "string" + } + }, + { + "name": "typedecrement", + "request": { + "type": "typedecrement", + "typed": { + "_arg": 0, + "type": "string" + }, + "param": { + "_arg": 1, + "type": "string" + } + }, + "response": { + "_retval": "string" + } + }, + { + "name": "selectioninfo", + "request": { + "type": "selectioninfo", + "typed": { + "_arg": 0, + "type": "string" + }, + "param": { + "_arg": 1, + "type": "string" + }, + "action": { + "_arg": 1, + "type": "string" + } + }, + "response": { + "_retval": "json" + } + } + ], + "events": {} + }, + "memory": { + "category": "actor", + "typeName": "memory", + "methods": [ + { + "name": "measure", + "request": { + "type": "measure" + }, + "response": { + "_retval": "json" + } + } + ], + "events": {} + }, + "eventLoopLag": { + "category": "actor", + "typeName": "eventLoopLag", + "methods": [ + { + "name": "start", + "request": { + "type": "start" + }, + "response": { + "success": { + "_retval": "number" + } + } + }, + { + "name": "stop", + "request": { + "type": "stop" + }, + "response": {} + } + ], + "events": { + "event-loop-lag": { + "type": "event-loop-lag", + "time": { + "_arg": 0, + "type": "number" + } + } + } + }, + "preference": { + "category": "actor", + "typeName": "preference", + "methods": [ + { + "name": "getBoolPref", + "request": { + "type": "getBoolPref", + "value": { + "_arg": 0, + "type": "primitive" + } + }, + "response": { + "value": { + "_retval": "boolean" + } + } + }, + { + "name": "getCharPref", + "request": { + "type": "getCharPref", + "value": { + "_arg": 0, + "type": "primitive" + } + }, + "response": { + "value": { + "_retval": "string" + } + } + }, + { + "name": "getIntPref", + "request": { + "type": "getIntPref", + "value": { + "_arg": 0, + "type": "primitive" + } + }, + "response": { + "value": { + "_retval": "number" + } + } + }, + { + "name": "getAllPrefs", + "request": { + "type": "getAllPrefs" + }, + "response": { + "value": { + "_retval": "json" + } + } + }, + { + "name": "setBoolPref", + "request": { + "type": "setBoolPref", + "name": { + "_arg": 0, + "type": "primitive" + }, + "value": { + "_arg": 1, + "type": "primitive" + } + }, + "response": {} + }, + { + "name": "setCharPref", + "request": { + "type": "setCharPref", + "name": { + "_arg": 0, + "type": "primitive" + }, + "value": { + "_arg": 1, + "type": "primitive" + } + }, + "response": {} + }, + { + "name": "setIntPref", + "request": { + "type": "setIntPref", + "name": { + "_arg": 0, + "type": "primitive" + }, + "value": { + "_arg": 1, + "type": "primitive" + } + }, + "response": {} + }, + { + "name": "clearUserPref", + "request": { + "type": "clearUserPref", + "name": { + "_arg": 0, + "type": "primitive" + } + }, + "response": {} + } + ], + "events": {} + }, + "device": { + "category": "actor", + "typeName": "device", + "methods": [ + { + "name": "getDescription", + "request": { + "type": "getDescription" + }, + "response": { + "value": { + "_retval": "json" + } + } + }, + { + "name": "getWallpaper", + "request": { + "type": "getWallpaper" + }, + "response": { + "value": { + "_retval": "longstring" + } + } + }, + { + "name": "screenshotToDataURL", + "request": { + "type": "screenshotToDataURL" + }, + "response": { + "value": { + "_retval": "longstring" + } + } + }, + { + "name": "getRawPermissionsTable", + "request": { + "type": "getRawPermissionsTable" + }, + "response": { + "value": { + "_retval": "json" + } + } + } + ], + "events": {} + } + }, + "from": "root" +} + +},{}],25:[function(_dereq_,module,exports){ +"use strict"; + +var Class = _dereq_("./class").Class; +var util = _dereq_("./util"); +var keys = util.keys; +var values = util.values; +var pairs = util.pairs; +var query = util.query; +var findPath = util.findPath; +var EventTarget = _dereq_("./event").EventTarget; + +var TypeSystem = Class({ + constructor: function(client) { + var types = Object.create(null); + var specification = Object.create(null); + + this.specification = specification; + this.types = types; + + var typeFor = function typeFor(typeName) { + typeName = typeName || "primitive"; + if (!types[typeName]) { + defineType(typeName); + } + + return types[typeName]; + }; + this.typeFor = typeFor; + + var defineType = function(descriptor) { + var type = void(0); + if (typeof(descriptor) === "string") { + if (descriptor.indexOf(":") > 0) + type = makeCompoundType(descriptor); + else if (descriptor.indexOf("#") > 0) + type = new ActorDetail(descriptor); + else if (specification[descriptor]) + type = makeCategoryType(specification[descriptor]); + } else { + type = makeCategoryType(descriptor); + } + + if (type) + types[type.name] = type; + else + throw TypeError("Invalid type: " + descriptor); + }; + this.defineType = defineType; + + + var makeCompoundType = function(name) { + var index = name.indexOf(":"); + var baseType = name.slice(0, index); + var subType = name.slice(index + 1); + + return baseType === "array" ? new ArrayOf(subType) : + baseType === "nullable" ? new Maybe(subType) : + null; + }; + + var makeCategoryType = function(descriptor) { + var category = descriptor.category; + return category === "dict" ? new Dictionary(descriptor) : + category === "actor" ? new Actor(descriptor) : + null; + }; + + var read = function(input, context, typeName) { + return typeFor(typeName).read(input, context); + } + this.read = read; + + var write = function(input, context, typeName) { + return typeFor(typeName).write(input); + }; + this.write = write; + + + var Type = Class({ + constructor: function() { + }, + get name() { + return this.category ? this.category + ":" + this.type : + this.type; + }, + read: function(input, context) { + throw new TypeError("`Type` subclass must implement `read`"); + }, + write: function(input, context) { + throw new TypeError("`Type` subclass must implement `write`"); + } + }); + + var Primitve = Class({ + extends: Type, + constuctor: function(type) { + this.type = type; + }, + read: function(input, context) { + return input; + }, + write: function(input, context) { + return input; + } + }); + + var Maybe = Class({ + extends: Type, + category: "nullable", + constructor: function(type) { + this.type = type; + }, + read: function(input, context) { + return input === null ? null : + input === void(0) ? void(0) : + read(input, context, this.type); + }, + write: function(input, context) { + return input === null ? null : + input === void(0) ? void(0) : + write(input, context, this.type); + } + }); + + var ArrayOf = Class({ + extends: Type, + category: "array", + constructor: function(type) { + this.type = type; + }, + read: function(input, context) { + var type = this.type; + return input.map(function($) { return read($, context, type) }); + }, + write: function(input, context) { + var type = this.type; + return input.map(function($) { return write($, context, type) }); + } + }); + + var makeField = function makeField(name, type) { + return { + enumerable: true, + configurable: true, + get: function() { + Object.defineProperty(this, name, { + configurable: false, + value: read(this.state[name], this.context, type) + }); + return this[name]; + } + } + }; + + var makeFields = function(descriptor) { + return pairs(descriptor).reduce(function(fields, pair) { + var name = pair[0], type = pair[1]; + fields[name] = makeField(name, type); + return fields; + }, {}); + } + + var DictionaryType = Class({}); + + var Dictionary = Class({ + extends: Type, + category: "dict", + get name() { return this.type; }, + constructor: function(descriptor) { + this.type = descriptor.typeName; + this.types = descriptor.specializations; + + var proto = Object.defineProperties({ + extends: DictionaryType, + constructor: function(state, context) { + Object.defineProperties(this, { + state: { + enumerable: false, + writable: true, + configurable: true, + value: state + }, + context: { + enumerable: false, + writable: false, + configurable: true, + value: context + } + }); + } + }, makeFields(this.types)); + + this.class = new Class(proto); + }, + read: function(input, context) { + return new this.class(input, context); + }, + write: function(input, context) { + var output = {}; + for (var key in input) { + output[key] = write(value, context, types[key]); + } + return output; + } + }); + + var makeMethods = function(descriptors) { + return descriptors.reduce(function(methods, descriptor) { + methods[descriptor.name] = { + enumerable: true, + configurable: true, + writable: false, + value: makeMethod(descriptor) + }; + return methods; + }, {}); + }; + + var makeEvents = function(descriptors) { + return pairs(descriptors).reduce(function(events, pair) { + var name = pair[0], descriptor = pair[1]; + var event = new Event(name, descriptor); + events[event.eventType] = event; + return events; + }, Object.create(null)); + }; + + var Actor = Class({ + extends: Type, + category: "actor", + get name() { return this.type; }, + constructor: function(descriptor) { + this.type = descriptor.typeName; + + var events = makeEvents(descriptor.events || {}); + var fields = makeFields(descriptor.fields || {}); + var methods = makeMethods(descriptor.methods || []); + + + var proto = { + extends: Front, + constructor: function() { + Front.apply(this, arguments); + }, + events: events + }; + Object.defineProperties(proto, fields); + Object.defineProperties(proto, methods); + + this.class = Class(proto); + }, + read: function(input, context, detail) { + var state = typeof(input) === "string" ? { actor: input } : input; + + var actor = client.get(state.actor) || new this.class(state, context); + actor.form(state, detail, context); + + return actor; + }, + write: function(input, context, detail) { + return input.id; + } + }); + exports.Actor = Actor; + + + var ActorDetail = Class({ + extends: Actor, + constructor: function(name) { + var parts = name.split("#") + this.actorType = parts[0] + this.detail = parts[1]; + }, + read: function(input, context) { + return typeFor(this.actorType).read(input, context, this.detail); + }, + write: function(input, context) { + return typeFor(this.actorType).write(input, context, this.detail); + } + }); + exports.ActorDetail = ActorDetail; + + var Method = Class({ + extends: Type, + constructor: function(descriptor) { + this.type = descriptor.name; + this.path = findPath(descriptor.response, "_retval"); + this.responseType = this.path && query(descriptor.response, this.path)._retval; + this.requestType = descriptor.request.type; + + var params = []; + for (var key in descriptor.request) { + if (key !== "type") { + var param = descriptor.request[key]; + var index = "_arg" in param ? param._arg : param._option; + var isParam = param._option === index; + var isArgument = param._arg === index; + params[index] = { + type: param.type, + key: key, + index: index, + isParam: isParam, + isArgument: isArgument + }; + } + } + this.params = params; + }, + read: function(input, context) { + return read(query(input, this.path), context, this.responseType); + }, + write: function(input, context) { + return this.params.reduce(function(result, param) { + result[param.key] = write(input[param.index], context, param.type); + return result; + }, {type: this.type}); + } + }); + exports.Method = Method; + + var profiler = function(method, id) { + return function() { + var start = new Date(); + return method.apply(this, arguments).then(function(result) { + var end = new Date(); + client.telemetry.add(id, +end - start); + return result; + }); + }; + }; + + var destructor = function(method) { + return function() { + return method.apply(this, arguments).then(function(result) { + client.release(this); + return result; + }); + }; + }; + + function makeMethod(descriptor) { + var type = new Method(descriptor); + var method = descriptor.oneway ? makeUnidirecationalMethod(descriptor, type) : + makeBidirectionalMethod(descriptor, type); + + if (descriptor.telemetry) + method = profiler(method); + if (descriptor.release) + method = destructor(method); + + return method; + } + + var makeUnidirecationalMethod = function(descriptor, type) { + return function() { + var packet = type.write(arguments, this); + packet.to = this.id; + client.send(packet); + return Promise.resolve(void(0)); + }; + }; + + var makeBidirectionalMethod = function(descriptor, type) { + return function() { + var context = this.context; + var packet = type.write(arguments, context); + var context = this.context; + packet.to = this.id; + return client.request(packet).then(function(packet) { + return type.read(packet, context); + }); + }; + }; + + var Event = Class({ + constructor: function(name, descriptor) { + this.name = descriptor.type || name; + this.eventType = descriptor.type || name; + this.types = Object.create(null); + + var types = this.types; + for (var key in descriptor) { + if (key === "type") { + types[key] = "string"; + } else { + types[key] = descriptor[key].type; + } + } + }, + read: function(input, context) { + var output = {}; + var types = this.types; + for (var key in input) { + output[key] = read(input[key], context, types[key]); + } + return output; + }, + write: function(input, context) { + var output = {}; + var types = this.types; + for (var key in this.types) { + output[key] = write(input[key], context, types[key]); + } + return output; + } + }); + + var Front = Class({ + extends: EventTarget, + EventTarget: EventTarget, + constructor: function(state) { + this.EventTarget(); + Object.defineProperties(this, { + state: { + enumerable: false, + writable: true, + configurable: true, + value: state + } + }); + + client.register(this); + }, + get id() { + return this.state.actor; + }, + get context() { + return this; + }, + form: function(state, detail, context) { + if (this.state !== state) { + if (detail) { + this.state[detail] = state[detail]; + } else { + pairs(state).forEach(function(pair) { + var key = pair[0], value = pair[1]; + this.state[key] = value; + }, this); + } + } + + if (context) { + client.supervise(context, this); + } + }, + requestTypes: function() { + return client.request({ + to: this.id, + type: "requestTypes" + }).then(function(packet) { + return packet.requestTypes; + }); + } + }); + types.primitive = new Primitve("primitive"); + types.string = new Primitve("string"); + types.number = new Primitve("number"); + types.boolean = new Primitve("boolean"); + types.json = new Primitve("json"); + types.array = new Primitve("array"); + }, + registerTypes: function(descriptor) { + var specification = this.specification; + values(descriptor.types).forEach(function(descriptor) { + specification[descriptor.typeName] = descriptor; + }); + } +}); +exports.TypeSystem = TypeSystem; + +},{"./class":3,"./event":5,"./util":26}],26:[function(_dereq_,module,exports){ +"use strict"; + +var keys = Object.keys; +exports.keys = keys; + +// Returns array of values for the given object. +var values = function(object) { + return keys(object).map(function(key) { + return object[key] + }); +}; +exports.values = values; + +// Returns [key, value] pairs for the given object. +var pairs = function(object) { + return keys(object).map(function(key) { + return [key, object[key]] + }); +}; +exports.pairs = pairs; + + +// Queries an object for the field nested with in it. +var query = function(object, path) { + return path.reduce(function(object, entry) { + return object && object[entry] + }, object); +}; +exports.query = query; + +var isObject = function(x) { + return x && typeof(x) === "object" +} + +var findPath = function(object, key) { + var path = void(0); + if (object && typeof(object) === "object") { + var names = keys(object); + if (names.indexOf(key) >= 0) { + path = []; + } else { + var index = 0; + var count = names.length; + while (index < count && !path){ + var head = names[index]; + var tail = findPath(object[head], key); + path = tail ? [head].concat(tail) : tail; + index = index + 1 + } + } + } + return path; +}; +exports.findPath = findPath; + +},{}]},{},[1]) +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2VuZXJhdGVkLmpzIiwic291cmNlcyI6WyIvVXNlcnMvZ296YWxhL1Byb2plY3RzL3ZvbGNhbi9ub2RlX21vZHVsZXMvYnJvd3NlcmlmeS9ub2RlX21vZHVsZXMvYnJvd3Nlci1wYWNrL19wcmVsdWRlLmpzIiwiL1VzZXJzL2dvemFsYS9Qcm9qZWN0cy92b2xjYW4vYnJvd3Nlci9pbmRleC5qcyIsIi9Vc2Vycy9nb3phbGEvUHJvamVjdHMvdm9sY2FuL2Jyb3dzZXIvcHJvbWlzZS5qcyIsIi9Vc2Vycy9nb3phbGEvUHJvamVjdHMvdm9sY2FuL2NsYXNzLmpzIiwiL1VzZXJzL2dvemFsYS9Qcm9qZWN0cy92b2xjYW4vY2xpZW50LmpzIiwiL1VzZXJzL2dvemFsYS9Qcm9qZWN0cy92b2xjYW4vZXZlbnQuanMiLCIvVXNlcnMvZ296YWxhL1Byb2plY3RzL3ZvbGNhbi9ub2RlX21vZHVsZXMvYnJvd3NlcmlmeS9ub2RlX21vZHVsZXMvZXZlbnRzL2V2ZW50cy5qcyIsIi9Vc2Vycy9nb3phbGEvUHJvamVjdHMvdm9sY2FuL25vZGVfbW9kdWxlcy9lczYtc3ltYm9sL2luZGV4LmpzIiwiL1VzZXJzL2dvemFsYS9Qcm9qZWN0cy92b2xjYW4vbm9kZV9tb2R1bGVzL2VzNi1zeW1ib2wvaXMtaW1wbGVtZW50ZWQuanMiLCIvVXNlcnMvZ296YWxhL1Byb2plY3RzL3ZvbGNhbi9ub2RlX21vZHVsZXMvZXM2LXN5bWJvbC9ub2RlX21vZHVsZXMvZC9pbmRleC5qcyIsIi9Vc2Vycy9nb3phbGEvUHJvamVjdHMvdm9sY2FuL25vZGVfbW9kdWxlcy9lczYtc3ltYm9sL25vZGVfbW9kdWxlcy9lczUtZXh0L29iamVjdC9hc3NpZ24vaW5kZXguanMiLCIvVXNlcnMvZ296YWxhL1Byb2plY3RzL3ZvbGNhbi9ub2RlX21vZHVsZXMvZXM2LXN5bWJvbC9ub2RlX21vZHVsZXMvZXM1LWV4dC9vYmplY3QvYXNzaWduL2lzLWltcGxlbWVudGVkLmpzIiwiL1VzZXJzL2dvemFsYS9Qcm9qZWN0cy92b2xjYW4vbm9kZV9tb2R1bGVzL2VzNi1zeW1ib2wvbm9kZV9tb2R1bGVzL2VzNS1leHQvb2JqZWN0L2Fzc2lnbi9zaGltLmpzIiwiL1VzZXJzL2dvemFsYS9Qcm9qZWN0cy92b2xjYW4vbm9kZV9tb2R1bGVzL2VzNi1zeW1ib2wvbm9kZV9tb2R1bGVzL2VzNS1leHQvb2JqZWN0L2lzLWNhbGxhYmxlLmpzIiwiL1VzZXJzL2dvemFsYS9Qcm9qZWN0cy92b2xjYW4vbm9kZV9tb2R1bGVzL2VzNi1zeW1ib2wvbm9kZV9tb2R1bGVzL2VzNS1leHQvb2JqZWN0L2tleXMvaW5kZXguanMiLCIvVXNlcnMvZ296YWxhL1Byb2plY3RzL3ZvbGNhbi9ub2RlX21vZHVsZXMvZXM2LXN5bWJvbC9ub2RlX21vZHVsZXMvZXM1LWV4dC9vYmplY3Qva2V5cy9pcy1pbXBsZW1lbnRlZC5qcyIsIi9Vc2Vycy9nb3phbGEvUHJvamVjdHMvdm9sY2FuL25vZGVfbW9kdWxlcy9lczYtc3ltYm9sL25vZGVfbW9kdWxlcy9lczUtZXh0L29iamVjdC9rZXlzL3NoaW0uanMiLCIvVXNlcnMvZ296YWxhL1Byb2plY3RzL3ZvbGNhbi9ub2RlX21vZHVsZXMvZXM2LXN5bWJvbC9ub2RlX21vZHVsZXMvZXM1LWV4dC9vYmplY3Qvbm9ybWFsaXplLW9wdGlvbnMuanMiLCIvVXNlcnMvZ296YWxhL1Byb2plY3RzL3ZvbGNhbi9ub2RlX21vZHVsZXMvZXM2LXN5bWJvbC9ub2RlX21vZHVsZXMvZXM1LWV4dC9vYmplY3QvdmFsaWQtdmFsdWUuanMiLCIvVXNlcnMvZ296YWxhL1Byb2plY3RzL3ZvbGNhbi9ub2RlX21vZHVsZXMvZXM2LXN5bWJvbC9ub2RlX21vZHVsZXMvZXM1LWV4dC9zdHJpbmcvIy9jb250YWlucy9pbmRleC5qcyIsIi9Vc2Vycy9nb3phbGEvUHJvamVjdHMvdm9sY2FuL25vZGVfbW9kdWxlcy9lczYtc3ltYm9sL25vZGVfbW9kdWxlcy9lczUtZXh0L3N0cmluZy8jL2NvbnRhaW5zL2lzLWltcGxlbWVudGVkLmpzIiwiL1VzZXJzL2dvemFsYS9Qcm9qZWN0cy92b2xjYW4vbm9kZV9tb2R1bGVzL2VzNi1zeW1ib2wvbm9kZV9tb2R1bGVzL2VzNS1leHQvc3RyaW5nLyMvY29udGFpbnMvc2hpbS5qcyIsIi9Vc2Vycy9nb3phbGEvUHJvamVjdHMvdm9sY2FuL25vZGVfbW9kdWxlcy9lczYtc3ltYm9sL3BvbHlmaWxsLmpzIiwiL1VzZXJzL2dvemFsYS9Qcm9qZWN0cy92b2xjYW4vc3BlY2lmaWNhdGlvbi9jb3JlLmpzb24iLCIvVXNlcnMvZ296YWxhL1Byb2plY3RzL3ZvbGNhbi9zcGVjaWZpY2F0aW9uL3Byb3RvY29sLmpzb24iLCIvVXNlcnMvZ296YWxhL1Byb2plY3RzL3ZvbGNhbi90eXBlLXN5c3RlbS5qcyIsIi9Vc2Vycy9nb3phbGEvUHJvamVjdHMvdm9sY2FuL3V0aWwuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUNBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNUQTtBQUNBO0FBQ0E7QUFDQTs7QUNIQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3RCQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDaExBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDbkVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQy9TQTtBQUNBO0FBQ0E7QUFDQTs7QUNIQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNyQkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDL0RBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNMQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNUQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3RCQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDTEE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ0xBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNSQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ1BBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDdEJBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ05BO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNMQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDUkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNQQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDckRBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3pKQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUMzdUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDcmRBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlc0NvbnRlbnQiOlsiKGZ1bmN0aW9uIGUodCxuLHIpe2Z1bmN0aW9uIHMobyx1KXtpZighbltvXSl7aWYoIXRbb10pe3ZhciBhPXR5cGVvZiByZXF1aXJlPT1cImZ1bmN0aW9uXCImJnJlcXVpcmU7aWYoIXUmJmEpcmV0dXJuIGEobywhMCk7aWYoaSlyZXR1cm4gaShvLCEwKTt0aHJvdyBuZXcgRXJyb3IoXCJDYW5ub3QgZmluZCBtb2R1bGUgJ1wiK28rXCInXCIpfXZhciBmPW5bb109e2V4cG9ydHM6e319O3Rbb11bMF0uY2FsbChmLmV4cG9ydHMsZnVuY3Rpb24oZSl7dmFyIG49dFtvXVsxXVtlXTtyZXR1cm4gcyhuP246ZSl9LGYsZi5leHBvcnRzLGUsdCxuLHIpfXJldHVybiBuW29dLmV4cG9ydHN9dmFyIGk9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtmb3IodmFyIG89MDtvPHIubGVuZ3RoO28rKylzKHJbb10pO3JldHVybiBzfSkiLCJcInVzZSBzdHJpY3RcIjtcblxudmFyIENsaWVudCA9IHJlcXVpcmUoXCIuLi9jbGllbnRcIikuQ2xpZW50O1xuXG5mdW5jdGlvbiBjb25uZWN0KHBvcnQpIHtcbiAgdmFyIGNsaWVudCA9IG5ldyBDbGllbnQoKTtcbiAgcmV0dXJuIGNsaWVudC5jb25uZWN0KHBvcnQpO1xufVxuZXhwb3J0cy5jb25uZWN0ID0gY29ubmVjdDtcbiIsIlwidXNlIHN0cmljdFwiO1xuXG5leHBvcnRzLlByb21pc2UgPSBQcm9taXNlO1xuIiwiXCJ1c2Ugc3RyaWN0XCI7XG5cbnZhciBkZXNjcmliZSA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3I7XG52YXIgQ2xhc3MgPSBmdW5jdGlvbihmaWVsZHMpIHtcbiAgdmFyIG5hbWVzID0gT2JqZWN0LmtleXMoZmllbGRzKTtcbiAgdmFyIGNvbnN0cnVjdG9yID0gbmFtZXMuaW5kZXhPZihcImNvbnN0cnVjdG9yXCIpID49IDAgPyBmaWVsZHMuY29uc3RydWN0b3IgOlxuICAgICAgICAgICAgICAgICAgICBmdW5jdGlvbigpIHt9O1xuICB2YXIgYW5jZXN0b3IgPSBmaWVsZHMuZXh0ZW5kcyB8fCBPYmplY3Q7XG5cbiAgdmFyIGRlc2NyaXB0b3IgPSBuYW1lcy5yZWR1Y2UoZnVuY3Rpb24oZGVzY3JpcHRvciwga2V5KSB7XG4gICAgZGVzY3JpcHRvcltrZXldID0gZGVzY3JpYmUoZmllbGRzLCBrZXkpO1xuICAgIHJldHVybiBkZXNjcmlwdG9yO1xuICB9LCB7fSk7XG5cbiAgdmFyIHByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoYW5jZXN0b3IucHJvdG90eXBlLCBkZXNjcmlwdG9yKTtcblxuICBjb25zdHJ1Y3Rvci5wcm90b3R5cGUgPSBwcm90b3R5cGU7XG4gIHByb3RvdHlwZS5jb25zdHJ1Y3RvciA9IGNvbnN0cnVjdG9yO1xuXG4gIHJldHVybiBjb25zdHJ1Y3Rvcjtcbn07XG5leHBvcnRzLkNsYXNzID0gQ2xhc3M7XG4iLCJcInVzZSBzdHJpY3RcIjtcblxudmFyIENsYXNzID0gcmVxdWlyZShcIi4vY2xhc3NcIikuQ2xhc3M7XG52YXIgVHlwZVN5c3RlbSA9IHJlcXVpcmUoXCIuL3R5cGUtc3lzdGVtXCIpLlR5cGVTeXN0ZW07XG52YXIgdmFsdWVzID0gcmVxdWlyZShcIi4vdXRpbFwiKS52YWx1ZXM7XG52YXIgUHJvbWlzZSA9IHJlcXVpcmUoXCJlczYtcHJvbWlzZVwiKS5Qcm9taXNlO1xudmFyIE1lc3NhZ2VFdmVudCA9IHJlcXVpcmUoXCIuL2V2ZW50XCIpLk1lc3NhZ2VFdmVudDtcblxudmFyIHNwZWNpZmljYXRpb24gPSByZXF1aXJlKFwiLi9zcGVjaWZpY2F0aW9uL2NvcmUuanNvblwiKTtcblxuZnVuY3Rpb24gcmVjb3ZlckFjdG9yRGVzY3JpcHRpb25zKGVycm9yKSB7XG4gIGNvbnNvbGUud2FybihcIkZhaWxlZCB0byBmZXRjaCBwcm90b2NvbCBzcGVjaWZpY2F0aW9uIChzZWUgcmVhc29uIGJlbG93KS4gXCIgK1xuICAgICAgICAgICAgICAgXCJVc2luZyBhIGZhbGxiYWNrIHByb3RvY2FsIHNwZWNpZmljYXRpb24hXCIsXG4gICAgICAgICAgICAgICBlcnJvcik7XG4gIHJldHVybiByZXF1aXJlKFwiLi9zcGVjaWZpY2F0aW9uL3Byb3RvY29sLmpzb25cIik7XG59XG5cbi8vIFR5cGUgdG8gcmVwcmVzZW50IHN1cGVydmlzZXIgYWN0b3IgcmVsYXRpb25zIHRvIGFjdG9ycyB0aGV5IHN1cGVydmlzZVxuLy8gaW4gdGVybXMgb2YgbGlmZXRpbWUgbWFuYWdlbWVudC5cbnZhciBTdXBlcnZpc29yID0gQ2xhc3Moe1xuICBjb25zdHJ1Y3RvcjogZnVuY3Rpb24oaWQpIHtcbiAgICB0aGlzLmlkID0gaWQ7XG4gICAgdGhpcy53b3JrZXJzID0gW107XG4gIH1cbn0pO1xuXG52YXIgVGVsZW1ldHJ5ID0gQ2xhc3Moe1xuICBhZGQ6IGZ1bmN0aW9uKGlkLCBtcykge1xuICAgIGNvbnNvbGUubG9nKFwidGVsZW1ldHJ5OjpcIiwgaWQsIG1zKVxuICB9XG59KTtcblxuLy8gQ29uc2lkZXIgbWFraW5nIGNsaWVudCBhIHJvb3QgYWN0b3IuXG5cbnZhciBDbGllbnQgPSBDbGFzcyh7XG4gIGNvbnN0cnVjdG9yOiBmdW5jdGlvbigpIHtcbiAgICB0aGlzLnJvb3QgPSBudWxsO1xuICAgIHRoaXMudGVsZW1ldHJ5ID0gbmV3IFRlbGVtZXRyeSgpO1xuXG4gICAgdGhpcy5zZXR1cENvbm5lY3Rpb24oKTtcbiAgICB0aGlzLnNldHVwTGlmZU1hbmFnZW1lbnQoKTtcbiAgICB0aGlzLnNldHVwVHlwZVN5c3RlbSgpO1xuICB9LFxuXG4gIHNldHVwQ29ubmVjdGlvbjogZnVuY3Rpb24oKSB7XG4gICAgdGhpcy5yZXF1ZXN0cyA9IFtdO1xuICB9LFxuICBzZXR1cExpZmVNYW5hZ2VtZW50OiBmdW5jdGlvbigpIHtcbiAgICB0aGlzLmNhY2hlID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcbiAgICB0aGlzLmdyYXBoID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcbiAgICB0aGlzLmdldCA9IHRoaXMuZ2V0LmJpbmQodGhpcyk7XG4gICAgdGhpcy5yZWxlYXNlID0gdGhpcy5yZWxlYXNlLmJpbmQodGhpcyk7XG4gIH0sXG4gIHNldHVwVHlwZVN5c3RlbTogZnVuY3Rpb24oKSB7XG4gICAgdGhpcy50eXBlU3lzdGVtID0gbmV3IFR5cGVTeXN0ZW0odGhpcyk7XG4gICAgdGhpcy50eXBlU3lzdGVtLnJlZ2lzdGVyVHlwZXMoc3BlY2lmaWNhdGlvbik7XG4gIH0sXG5cbiAgY29ubmVjdDogZnVuY3Rpb24ocG9ydCkge1xuICAgIHZhciBjbGllbnQgPSB0aGlzO1xuICAgIHJldHVybiBuZXcgUHJvbWlzZShmdW5jdGlvbihyZXNvbHZlLCByZWplY3QpIHtcbiAgICAgIGNsaWVudC5wb3J0ID0gcG9ydDtcbiAgICAgIHBvcnQub25tZXNzYWdlID0gY2xpZW50LnJlY2VpdmUuYmluZChjbGllbnQpO1xuICAgICAgY2xpZW50Lm9uUmVhZHkgPSByZXNvbHZlO1xuICAgICAgY2xpZW50Lm9uRmFpbCA9IHJlamVjdDtcblxuICAgICAgcG9ydC5zdGFydCgpO1xuICAgIH0pO1xuICB9LFxuICBzZW5kOiBmdW5jdGlvbihwYWNrZXQpIHtcbiAgICB0aGlzLnBvcnQucG9zdE1lc3NhZ2UocGFja2V0KTtcbiAgfSxcbiAgcmVxdWVzdDogZnVuY3Rpb24ocGFja2V0KSB7XG4gICAgdmFyIGNsaWVudCA9IHRoaXM7XG4gICAgcmV0dXJuIG5ldyBQcm9taXNlKGZ1bmN0aW9uKHJlc29sdmUsIHJlamVjdCkge1xuICAgICAgY2xpZW50LnJlcXVlc3RzLnB1c2gocGFja2V0LnRvLCB7IHJlc29sdmU6IHJlc29sdmUsIHJlamVjdDogcmVqZWN0IH0pO1xuICAgICAgY2xpZW50LnNlbmQocGFja2V0KTtcbiAgICB9KTtcbiAgfSxcblxuICByZWNlaXZlOiBmdW5jdGlvbihldmVudCkge1xuICAgIHZhciBwYWNrZXQgPSBldmVudC5kYXRhO1xuICAgIGlmICghdGhpcy5yb290KSB7XG4gICAgICBpZiAocGFja2V0LmZyb20gIT09IFwicm9vdFwiKVxuICAgICAgICB0aHJvdyBFcnJvcihcIkluaXRpYWwgcGFja2V0IG11c3QgYmUgZnJvbSByb290XCIpO1xuICAgICAgaWYgKCEoXCJhcHBsaWNhdGlvblR5cGVcIiBpbiBwYWNrZXQpKVxuICAgICAgICB0aHJvdyBFcnJvcihcIkluaXRpYWwgcGFja2V0IG11c3QgY29udGFpbiBhcHBsaWNhdGlvblR5cGUgZmllbGRcIik7XG5cbiAgICAgIHRoaXMucm9vdCA9IHRoaXMudHlwZVN5c3RlbS5yZWFkKFwicm9vdFwiLCBudWxsLCBcInJvb3RcIik7XG4gICAgICB0aGlzLnJvb3RcbiAgICAgICAgICAucHJvdG9jb2xEZXNjcmlwdGlvbigpXG4gICAgICAgICAgLmNhdGNoKHJlY292ZXJBY3RvckRlc2NyaXB0aW9ucylcbiAgICAgICAgICAudGhlbih0aGlzLnR5cGVTeXN0ZW0ucmVnaXN0ZXJUeXBlcy5iaW5kKHRoaXMudHlwZVN5c3RlbSkpXG4gICAgICAgICAgLnRoZW4odGhpcy5vblJlYWR5LmJpbmQodGhpcywgdGhpcy5yb290KSwgdGhpcy5vbkZhaWwpO1xuICAgIH0gZWxzZSB7XG4gICAgICB2YXIgYWN0b3IgPSB0aGlzLmdldChwYWNrZXQuZnJvbSkgfHwgdGhpcy5yb290O1xuICAgICAgdmFyIGV2ZW50ID0gYWN0b3IuZXZlbnRzW3BhY2tldC50eXBlXTtcbiAgICAgIGlmIChldmVudCkge1xuICAgICAgICB2YXIgbWVzc2FnZSA9IG5ldyBNZXNzYWdlRXZlbnQocGFja2V0LnR5cGUsIHtcbiAgICAgICAgICBkYXRhOiBldmVudC5yZWFkKHBhY2tldClcbiAgICAgICAgfSk7XG4gICAgICAgIGFjdG9yLmRpc3BhdGNoRXZlbnQobWVzc2FnZSk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB2YXIgaW5kZXggPSB0aGlzLnJlcXVlc3RzLmluZGV4T2YoYWN0b3IuaWQpO1xuICAgICAgICBpZiAoaW5kZXggPj0gMCkge1xuICAgICAgICAgIHZhciByZXF1ZXN0ID0gdGhpcy5yZXF1ZXN0cy5zcGxpY2UoaW5kZXgsIDIpLnBvcCgpO1xuICAgICAgICAgIGlmIChwYWNrZXQuZXJyb3IpXG4gICAgICAgICAgICByZXF1ZXN0LnJlamVjdChwYWNrZXQpO1xuICAgICAgICAgIGVsc2VcbiAgICAgICAgICAgIHJlcXVlc3QucmVzb2x2ZShwYWNrZXQpO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIGNvbnNvbGUuZXJyb3IoRXJyb3IoXCJVbmV4cGVjdGVkIHBhY2tldCBcIiArIEpTT04uc3RyaW5naWZ5KHBhY2tldCwgMiwgMikpLFxuICAgICAgICAgICAgICAgICAgICAgICAgcGFja2V0LFxuICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5yZXF1ZXN0cy5zbGljZSgwKSk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gIH0sXG5cbiAgZ2V0OiBmdW5jdGlvbihpZCkge1xuICAgIHJldHVybiB0aGlzLmNhY2hlW2lkXTtcbiAgfSxcbiAgc3VwZXJ2aXNvck9mOiBmdW5jdGlvbihhY3Rvcikge1xuICAgIGZvciAodmFyIGlkIGluIHRoaXMuZ3JhcGgpIHtcbiAgICAgIGlmICh0aGlzLmdyYXBoW2lkXS5pbmRleE9mKGFjdG9yLmlkKSA+PSAwKSB7XG4gICAgICAgIHJldHVybiBpZDtcbiAgICAgIH1cbiAgICB9XG4gIH0sXG4gIHdvcmtlcnNPZjogZnVuY3Rpb24oYWN0b3IpIHtcbiAgICByZXR1cm4gdGhpcy5ncmFwaFthY3Rvci5pZF07XG4gIH0sXG4gIHN1cGVydmlzZTogZnVuY3Rpb24oYWN0b3IsIHdvcmtlcikge1xuICAgIHZhciB3b3JrZXJzID0gdGhpcy53b3JrZXJzT2YoYWN0b3IpXG4gICAgaWYgKHdvcmtlcnMuaW5kZXhPZih3b3JrZXIuaWQpIDwgMCkge1xuICAgICAgd29ya2Vycy5wdXNoKHdvcmtlci5pZCk7XG4gICAgfVxuICB9LFxuICB1bnN1cGVydmlzZTogZnVuY3Rpb24oYWN0b3IsIHdvcmtlcikge1xuICAgIHZhciB3b3JrZXJzID0gdGhpcy53b3JrZXJzT2YoYWN0b3IpO1xuICAgIHZhciBpbmRleCA9IHdvcmtlcnMuaW5kZXhPZih3b3JrZXIuaWQpXG4gICAgaWYgKGluZGV4ID49IDApIHtcbiAgICAgIHdvcmtlcnMuc3BsaWNlKGluZGV4LCAxKVxuICAgIH1cbiAgfSxcblxuICByZWdpc3RlcjogZnVuY3Rpb24oYWN0b3IpIHtcbiAgICB2YXIgcmVnaXN0ZXJlZCA9IHRoaXMuZ2V0KGFjdG9yLmlkKTtcbiAgICBpZiAoIXJlZ2lzdGVyZWQpIHtcbiAgICAgIHRoaXMuY2FjaGVbYWN0b3IuaWRdID0gYWN0b3I7XG4gICAgICB0aGlzLmdyYXBoW2FjdG9yLmlkXSA9IFtdO1xuICAgIH0gZWxzZSBpZiAocmVnaXN0ZXJlZCAhPT0gYWN0b3IpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcihcIkRpZmZlcmVudCBhY3RvciB3aXRoIHNhbWUgaWQgaXMgYWxyZWFkeSByZWdpc3RlcmVkXCIpO1xuICAgIH1cbiAgfSxcbiAgdW5yZWdpc3RlcjogZnVuY3Rpb24oYWN0b3IpIHtcbiAgICBpZiAodGhpcy5nZXQoYWN0b3IuaWQpKSB7XG4gICAgICBkZWxldGUgdGhpcy5jYWNoZVthY3Rvci5pZF07XG4gICAgICBkZWxldGUgdGhpcy5ncmFwaFthY3Rvci5pZF07XG4gICAgfVxuICB9LFxuXG4gIHJlbGVhc2U6IGZ1bmN0aW9uKGFjdG9yKSB7XG4gICAgdmFyIHN1cGVydmlzb3IgPSB0aGlzLnN1cGVydmlzb3JPZihhY3Rvcik7XG4gICAgaWYgKHN1cGVydmlzb3IpXG4gICAgICB0aGlzLnVuc3VwZXJ2aXNlKHN1cGVydmlzb3IsIGFjdG9yKTtcblxuICAgIHZhciB3b3JrZXJzID0gdGhpcy53b3JrZXJzT2YoYWN0b3IpXG5cbiAgICBpZiAod29ya2Vycykge1xuICAgICAgd29ya2Vycy5tYXAodGhpcy5nZXQpLmZvckVhY2godGhpcy5yZWxlYXNlKVxuICAgIH1cbiAgICB0aGlzLnVucmVnaXN0ZXIoYWN0b3IpO1xuICB9XG59KTtcbmV4cG9ydHMuQ2xpZW50ID0gQ2xpZW50O1xuIiwiXCJ1c2Ugc3RyaWN0XCI7XG5cbnZhciBTeW1ib2wgPSByZXF1aXJlKFwiZXM2LXN5bWJvbFwiKVxudmFyIEV2ZW50RW1pdHRlciA9IHJlcXVpcmUoXCJldmVudHNcIikuRXZlbnRFbWl0dGVyO1xudmFyIENsYXNzID0gcmVxdWlyZShcIi4vY2xhc3NcIikuQ2xhc3M7XG5cbnZhciAkYm91bmQgPSBTeW1ib2woXCJFdmVudFRhcmdldC9oYW5kbGVFdmVudFwiKTtcbnZhciAkZW1pdHRlciA9IFN5bWJvbChcIkV2ZW50VGFyZ2V0L2VtaXR0ZXJcIik7XG5cbmZ1bmN0aW9uIG1ha2VIYW5kbGVyKGhhbmRsZXIpIHtcbiAgcmV0dXJuIGZ1bmN0aW9uKGV2ZW50KSB7XG4gICAgaGFuZGxlci5oYW5kbGVFdmVudChldmVudCk7XG4gIH1cbn1cblxudmFyIEV2ZW50VGFyZ2V0ID0gQ2xhc3Moe1xuICBjb25zdHJ1Y3RvcjogZnVuY3Rpb24oKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KHRoaXMsICRlbWl0dGVyLCB7XG4gICAgICBlbnVtZXJhYmxlOiBmYWxzZSxcbiAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZSxcbiAgICAgIHdyaXRhYmxlOiB0cnVlLFxuICAgICAgdmFsdWU6IG5ldyBFdmVudEVtaXR0ZXIoKVxuICAgIH0pO1xuICB9LFxuICBhZGRFdmVudExpc3RlbmVyOiBmdW5jdGlvbih0eXBlLCBoYW5kbGVyKSB7XG4gICAgaWYgKHR5cGVvZihoYW5kbGVyKSA9PT0gXCJmdW5jdGlvblwiKSB7XG4gICAgICB0aGlzWyRlbWl0dGVyXS5vbih0eXBlLCBoYW5kbGVyKTtcbiAgICB9XG4gICAgZWxzZSBpZiAoaGFuZGxlciAmJiB0eXBlb2YoaGFuZGxlcikgPT09IFwib2JqZWN0XCIpIHtcbiAgICAgIGlmICghaGFuZGxlclskYm91bmRdKSBoYW5kbGVyWyRib3VuZF0gPSBtYWtlSGFuZGxlcihoYW5kbGVyKTtcbiAgICAgIHRoaXNbJGVtaXR0ZXJdLm9uKHR5cGUsIGhhbmRsZXJbJGJvdW5kXSk7XG4gICAgfVxuICB9LFxuICByZW1vdmVFdmVudExpc3RlbmVyOiBmdW5jdGlvbih0eXBlLCBoYW5kbGVyKSB7XG4gICAgaWYgKHR5cGVvZihoYW5kbGVyKSA9PT0gXCJmdW5jdGlvblwiKVxuICAgICAgdGhpc1skZW1pdHRlcl0ucmVtb3ZlTGlzdGVuZXIodHlwZSwgaGFuZGxlcik7XG4gICAgZWxzZSBpZiAoaGFuZGxlciAmJiBoYW5kbGVyWyRib3VuZF0pXG4gICAgICB0aGlzWyRlbWl0dGVyXS5yZW1vdmVMaXN0ZW5lcih0eXBlLCBoYW5kbGVyWyRib3VuZF0pO1xuICB9LFxuICBkaXNwYXRjaEV2ZW50OiBmdW5jdGlvbihldmVudCkge1xuICAgIGV2ZW50LnRhcmdldCA9IHRoaXM7XG4gICAgdGhpc1skZW1pdHRlcl0uZW1pdChldmVudC50eXBlLCBldmVudCk7XG4gIH1cbn0pO1xuZXhwb3J0cy5FdmVudFRhcmdldCA9IEV2ZW50VGFyZ2V0O1xuXG52YXIgTWVzc2FnZUV2ZW50ID0gQ2xhc3Moe1xuICBjb25zdHJ1Y3RvcjogZnVuY3Rpb24odHlwZSwgb3B0aW9ucykge1xuICAgIG9wdGlvbnMgPSBvcHRpb25zIHx8IHt9O1xuICAgIHRoaXMudHlwZSA9IHR5cGU7XG4gICAgdGhpcy5kYXRhID0gb3B0aW9ucy5kYXRhID09PSB2b2lkKDApID8gbnVsbCA6IG9wdGlvbnMuZGF0YTtcblxuICAgIHRoaXMubGFzdEV2ZW50SWQgPSBvcHRpb25zLmxhc3RFdmVudElkIHx8IFwiXCI7XG4gICAgdGhpcy5vcmlnaW4gPSBvcHRpb25zLm9yaWdpbiB8fCBcIlwiO1xuICAgIHRoaXMuYnViYmxlcyA9IG9wdGlvbnMuYnViYmxlcyB8fCBmYWxzZTtcbiAgICB0aGlzLmNhbmNlbGFibGUgPSBvcHRpb25zLmNhbmNlbGFibGUgfHwgZmFsc2U7XG4gIH0sXG4gIHNvdXJjZTogbnVsbCxcbiAgcG9ydHM6IG51bGwsXG4gIHByZXZlbnREZWZhdWx0OiBmdW5jdGlvbigpIHtcbiAgfSxcbiAgc3RvcFByb3BhZ2F0aW9uOiBmdW5jdGlvbigpIHtcbiAgfSxcbiAgc3RvcEltbWVkaWF0ZVByb3BhZ2F0aW9uOiBmdW5jdGlvbigpIHtcbiAgfVxufSk7XG5leHBvcnRzLk1lc3NhZ2VFdmVudCA9IE1lc3NhZ2VFdmVudDtcbiIsIi8vIENvcHlyaWdodCBKb3llbnQsIEluYy4gYW5kIG90aGVyIE5vZGUgY29udHJpYnV0b3JzLlxuLy9cbi8vIFBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhXG4vLyBjb3B5IG9mIHRoaXMgc29mdHdhcmUgYW5kIGFzc29jaWF0ZWQgZG9jdW1lbnRhdGlvbiBmaWxlcyAodGhlXG4vLyBcIlNvZnR3YXJlXCIpLCB0byBkZWFsIGluIHRoZSBTb2Z0d2FyZSB3aXRob3V0IHJlc3RyaWN0aW9uLCBpbmNsdWRpbmdcbi8vIHdpdGhvdXQgbGltaXRhdGlvbiB0aGUgcmlnaHRzIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBtZXJnZSwgcHVibGlzaCxcbi8vIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsIGNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXRcbi8vIHBlcnNvbnMgdG8gd2hvbSB0aGUgU29mdHdhcmUgaXMgZnVybmlzaGVkIHRvIGRvIHNvLCBzdWJqZWN0IHRvIHRoZVxuLy8gZm9sbG93aW5nIGNvbmRpdGlvbnM6XG4vL1xuLy8gVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWRcbi8vIGluIGFsbCBjb3BpZXMgb3Igc3Vic3RhbnRpYWwgcG9ydGlvbnMgb2YgdGhlIFNvZnR3YXJlLlxuLy9cbi8vIFRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCBcIkFTIElTXCIsIFdJVEhPVVQgV0FSUkFOVFkgT0YgQU5ZIEtJTkQsIEVYUFJFU1Ncbi8vIE9SIElNUExJRUQsIElOQ0xVRElORyBCVVQgTk9UIExJTUlURUQgVE8gVEhFIFdBUlJBTlRJRVMgT0Zcbi8vIE1FUkNIQU5UQUJJTElUWSwgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU5cbi8vIE5PIEVWRU5UIFNIQUxMIFRIRSBBVVRIT1JTIE9SIENPUFlSSUdIVCBIT0xERVJTIEJFIExJQUJMRSBGT1IgQU5ZIENMQUlNLFxuLy8gREFNQUdFUyBPUiBPVEhFUiBMSUFCSUxJVFksIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBUT1JUIE9SXG4vLyBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFXG4vLyBVU0UgT1IgT1RIRVIgREVBTElOR1MgSU4gVEhFIFNPRlRXQVJFLlxuXG5mdW5jdGlvbiBFdmVudEVtaXR0ZXIoKSB7XG4gIHRoaXMuX2V2ZW50cyA9IHRoaXMuX2V2ZW50cyB8fCB7fTtcbiAgdGhpcy5fbWF4TGlzdGVuZXJzID0gdGhpcy5fbWF4TGlzdGVuZXJzIHx8IHVuZGVmaW5lZDtcbn1cbm1vZHVsZS5leHBvcnRzID0gRXZlbnRFbWl0dGVyO1xuXG4vLyBCYWNrd2FyZHMtY29tcGF0IHdpdGggbm9kZSAwLjEwLnhcbkV2ZW50RW1pdHRlci5FdmVudEVtaXR0ZXIgPSBFdmVudEVtaXR0ZXI7XG5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUuX2V2ZW50cyA9IHVuZGVmaW5lZDtcbkV2ZW50RW1pdHRlci5wcm90b3R5cGUuX21heExpc3RlbmVycyA9IHVuZGVmaW5lZDtcblxuLy8gQnkgZGVmYXVsdCBFdmVudEVtaXR0ZXJzIHdpbGwgcHJpbnQgYSB3YXJuaW5nIGlmIG1vcmUgdGhhbiAxMCBsaXN0ZW5lcnMgYXJlXG4vLyBhZGRlZCB0byBpdC4gVGhpcyBpcyBhIHVzZWZ1bCBkZWZhdWx0IHdoaWNoIGhlbHBzIGZpbmRpbmcgbWVtb3J5IGxlYWtzLlxuRXZlbnRFbWl0dGVyLmRlZmF1bHRNYXhMaXN0ZW5lcnMgPSAxMDtcblxuLy8gT2J2aW91c2x5IG5vdCBhbGwgRW1pdHRlcnMgc2hvdWxkIGJlIGxpbWl0ZWQgdG8gMTAuIFRoaXMgZnVuY3Rpb24gYWxsb3dzXG4vLyB0aGF0IHRvIGJlIGluY3JlYXNlZC4gU2V0IHRvIHplcm8gZm9yIHVubGltaXRlZC5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUuc2V0TWF4TGlzdGVuZXJzID0gZnVuY3Rpb24obikge1xuICBpZiAoIWlzTnVtYmVyKG4pIHx8IG4gPCAwIHx8IGlzTmFOKG4pKVxuICAgIHRocm93IFR5cGVFcnJvcignbiBtdXN0IGJlIGEgcG9zaXRpdmUgbnVtYmVyJyk7XG4gIHRoaXMuX21heExpc3RlbmVycyA9IG47XG4gIHJldHVybiB0aGlzO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5lbWl0ID0gZnVuY3Rpb24odHlwZSkge1xuICB2YXIgZXIsIGhhbmRsZXIsIGxlbiwgYXJncywgaSwgbGlzdGVuZXJzO1xuXG4gIGlmICghdGhpcy5fZXZlbnRzKVxuICAgIHRoaXMuX2V2ZW50cyA9IHt9O1xuXG4gIC8vIElmIHRoZXJlIGlzIG5vICdlcnJvcicgZXZlbnQgbGlzdGVuZXIgdGhlbiB0aHJvdy5cbiAgaWYgKHR5cGUgPT09ICdlcnJvcicpIHtcbiAgICBpZiAoIXRoaXMuX2V2ZW50cy5lcnJvciB8fFxuICAgICAgICAoaXNPYmplY3QodGhpcy5fZXZlbnRzLmVycm9yKSAmJiAhdGhpcy5fZXZlbnRzLmVycm9yLmxlbmd0aCkpIHtcbiAgICAgIGVyID0gYXJndW1lbnRzWzFdO1xuICAgICAgaWYgKGVyIGluc3RhbmNlb2YgRXJyb3IpIHtcbiAgICAgICAgdGhyb3cgZXI7IC8vIFVuaGFuZGxlZCAnZXJyb3InIGV2ZW50XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aHJvdyBUeXBlRXJyb3IoJ1VuY2F1Z2h0LCB1bnNwZWNpZmllZCBcImVycm9yXCIgZXZlbnQuJyk7XG4gICAgICB9XG4gICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICB9XG5cbiAgaGFuZGxlciA9IHRoaXMuX2V2ZW50c1t0eXBlXTtcblxuICBpZiAoaXNVbmRlZmluZWQoaGFuZGxlcikpXG4gICAgcmV0dXJuIGZhbHNlO1xuXG4gIGlmIChpc0Z1bmN0aW9uKGhhbmRsZXIpKSB7XG4gICAgc3dpdGNoIChhcmd1bWVudHMubGVuZ3RoKSB7XG4gICAgICAvLyBmYXN0IGNhc2VzXG4gICAgICBjYXNlIDE6XG4gICAgICAgIGhhbmRsZXIuY2FsbCh0aGlzKTtcbiAgICAgICAgYnJlYWs7XG4gICAgICBjYXNlIDI6XG4gICAgICAgIGhhbmRsZXIuY2FsbCh0aGlzLCBhcmd1bWVudHNbMV0pO1xuICAgICAgICBicmVhaztcbiAgICAgIGNhc2UgMzpcbiAgICAgICAgaGFuZGxlci5jYWxsKHRoaXMsIGFyZ3VtZW50c1sxXSwgYXJndW1lbnRzWzJdKTtcbiAgICAgICAgYnJlYWs7XG4gICAgICAvLyBzbG93ZXJcbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIGxlbiA9IGFyZ3VtZW50cy5sZW5ndGg7XG4gICAgICAgIGFyZ3MgPSBuZXcgQXJyYXkobGVuIC0gMSk7XG4gICAgICAgIGZvciAoaSA9IDE7IGkgPCBsZW47IGkrKylcbiAgICAgICAgICBhcmdzW2kgLSAxXSA9IGFyZ3VtZW50c1tpXTtcbiAgICAgICAgaGFuZGxlci5hcHBseSh0aGlzLCBhcmdzKTtcbiAgICB9XG4gIH0gZWxzZSBpZiAoaXNPYmplY3QoaGFuZGxlcikpIHtcbiAgICBsZW4gPSBhcmd1bWVudHMubGVuZ3RoO1xuICAgIGFyZ3MgPSBuZXcgQXJyYXkobGVuIC0gMSk7XG4gICAgZm9yIChpID0gMTsgaSA8IGxlbjsgaSsrKVxuICAgICAgYXJnc1tpIC0gMV0gPSBhcmd1bWVudHNbaV07XG5cbiAgICBsaXN0ZW5lcnMgPSBoYW5kbGVyLnNsaWNlKCk7XG4gICAgbGVuID0gbGlzdGVuZXJzLmxlbmd0aDtcbiAgICBmb3IgKGkgPSAwOyBpIDwgbGVuOyBpKyspXG4gICAgICBsaXN0ZW5lcnNbaV0uYXBwbHkodGhpcywgYXJncyk7XG4gIH1cblxuICByZXR1cm4gdHJ1ZTtcbn07XG5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUuYWRkTGlzdGVuZXIgPSBmdW5jdGlvbih0eXBlLCBsaXN0ZW5lcikge1xuICB2YXIgbTtcblxuICBpZiAoIWlzRnVuY3Rpb24obGlzdGVuZXIpKVxuICAgIHRocm93IFR5cGVFcnJvcignbGlzdGVuZXIgbXVzdCBiZSBhIGZ1bmN0aW9uJyk7XG5cbiAgaWYgKCF0aGlzLl9ldmVudHMpXG4gICAgdGhpcy5fZXZlbnRzID0ge307XG5cbiAgLy8gVG8gYXZvaWQgcmVjdXJzaW9uIGluIHRoZSBjYXNlIHRoYXQgdHlwZSA9PT0gXCJuZXdMaXN0ZW5lclwiISBCZWZvcmVcbiAgLy8gYWRkaW5nIGl0IHRvIHRoZSBsaXN0ZW5lcnMsIGZpcnN0IGVtaXQgXCJuZXdMaXN0ZW5lclwiLlxuICBpZiAodGhpcy5fZXZlbnRzLm5ld0xpc3RlbmVyKVxuICAgIHRoaXMuZW1pdCgnbmV3TGlzdGVuZXInLCB0eXBlLFxuICAgICAgICAgICAgICBpc0Z1bmN0aW9uKGxpc3RlbmVyLmxpc3RlbmVyKSA/XG4gICAgICAgICAgICAgIGxpc3RlbmVyLmxpc3RlbmVyIDogbGlzdGVuZXIpO1xuXG4gIGlmICghdGhpcy5fZXZlbnRzW3R5cGVdKVxuICAgIC8vIE9wdGltaXplIHRoZSBjYXNlIG9mIG9uZSBsaXN0ZW5lci4gRG9uJ3QgbmVlZCB0aGUgZXh0cmEgYXJyYXkgb2JqZWN0LlxuICAgIHRoaXMuX2V2ZW50c1t0eXBlXSA9IGxpc3RlbmVyO1xuICBlbHNlIGlmIChpc09iamVjdCh0aGlzLl9ldmVudHNbdHlwZV0pKVxuICAgIC8vIElmIHdlJ3ZlIGFscmVhZHkgZ290IGFuIGFycmF5LCBqdXN0IGFwcGVuZC5cbiAgICB0aGlzLl9ldmVudHNbdHlwZV0ucHVzaChsaXN0ZW5lcik7XG4gIGVsc2VcbiAgICAvLyBBZGRpbmcgdGhlIHNlY29uZCBlbGVtZW50LCBuZWVkIHRvIGNoYW5nZSB0byBhcnJheS5cbiAgICB0aGlzLl9ldmVudHNbdHlwZV0gPSBbdGhpcy5fZXZlbnRzW3R5cGVdLCBsaXN0ZW5lcl07XG5cbiAgLy8gQ2hlY2sgZm9yIGxpc3RlbmVyIGxlYWtcbiAgaWYgKGlzT2JqZWN0KHRoaXMuX2V2ZW50c1t0eXBlXSkgJiYgIXRoaXMuX2V2ZW50c1t0eXBlXS53YXJuZWQpIHtcbiAgICB2YXIgbTtcbiAgICBpZiAoIWlzVW5kZWZpbmVkKHRoaXMuX21heExpc3RlbmVycykpIHtcbiAgICAgIG0gPSB0aGlzLl9tYXhMaXN0ZW5lcnM7XG4gICAgfSBlbHNlIHtcbiAgICAgIG0gPSBFdmVudEVtaXR0ZXIuZGVmYXVsdE1heExpc3RlbmVycztcbiAgICB9XG5cbiAgICBpZiAobSAmJiBtID4gMCAmJiB0aGlzLl9ldmVudHNbdHlwZV0ubGVuZ3RoID4gbSkge1xuICAgICAgdGhpcy5fZXZlbnRzW3R5cGVdLndhcm5lZCA9IHRydWU7XG4gICAgICBjb25zb2xlLmVycm9yKCcobm9kZSkgd2FybmluZzogcG9zc2libGUgRXZlbnRFbWl0dGVyIG1lbW9yeSAnICtcbiAgICAgICAgICAgICAgICAgICAgJ2xlYWsgZGV0ZWN0ZWQuICVkIGxpc3RlbmVycyBhZGRlZC4gJyArXG4gICAgICAgICAgICAgICAgICAgICdVc2UgZW1pdHRlci5zZXRNYXhMaXN0ZW5lcnMoKSB0byBpbmNyZWFzZSBsaW1pdC4nLFxuICAgICAgICAgICAgICAgICAgICB0aGlzLl9ldmVudHNbdHlwZV0ubGVuZ3RoKTtcbiAgICAgIGlmICh0eXBlb2YgY29uc29sZS50cmFjZSA9PT0gJ2Z1bmN0aW9uJykge1xuICAgICAgICAvLyBub3Qgc3VwcG9ydGVkIGluIElFIDEwXG4gICAgICAgIGNvbnNvbGUudHJhY2UoKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICByZXR1cm4gdGhpcztcbn07XG5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUub24gPSBFdmVudEVtaXR0ZXIucHJvdG90eXBlLmFkZExpc3RlbmVyO1xuXG5FdmVudEVtaXR0ZXIucHJvdG90eXBlLm9uY2UgPSBmdW5jdGlvbih0eXBlLCBsaXN0ZW5lcikge1xuICBpZiAoIWlzRnVuY3Rpb24obGlzdGVuZXIpKVxuICAgIHRocm93IFR5cGVFcnJvcignbGlzdGVuZXIgbXVzdCBiZSBhIGZ1bmN0aW9uJyk7XG5cbiAgdmFyIGZpcmVkID0gZmFsc2U7XG5cbiAgZnVuY3Rpb24gZygpIHtcbiAgICB0aGlzLnJlbW92ZUxpc3RlbmVyKHR5cGUsIGcpO1xuXG4gICAgaWYgKCFmaXJlZCkge1xuICAgICAgZmlyZWQgPSB0cnVlO1xuICAgICAgbGlzdGVuZXIuYXBwbHkodGhpcywgYXJndW1lbnRzKTtcbiAgICB9XG4gIH1cblxuICBnLmxpc3RlbmVyID0gbGlzdGVuZXI7XG4gIHRoaXMub24odHlwZSwgZyk7XG5cbiAgcmV0dXJuIHRoaXM7XG59O1xuXG4vLyBlbWl0cyBhICdyZW1vdmVMaXN0ZW5lcicgZXZlbnQgaWZmIHRoZSBsaXN0ZW5lciB3YXMgcmVtb3ZlZFxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5yZW1vdmVMaXN0ZW5lciA9IGZ1bmN0aW9uKHR5cGUsIGxpc3RlbmVyKSB7XG4gIHZhciBsaXN0LCBwb3NpdGlvbiwgbGVuZ3RoLCBpO1xuXG4gIGlmICghaXNGdW5jdGlvbihsaXN0ZW5lcikpXG4gICAgdGhyb3cgVHlwZUVycm9yKCdsaXN0ZW5lciBtdXN0IGJlIGEgZnVuY3Rpb24nKTtcblxuICBpZiAoIXRoaXMuX2V2ZW50cyB8fCAhdGhpcy5fZXZlbnRzW3R5cGVdKVxuICAgIHJldHVybiB0aGlzO1xuXG4gIGxpc3QgPSB0aGlzLl9ldmVudHNbdHlwZV07XG4gIGxlbmd0aCA9IGxpc3QubGVuZ3RoO1xuICBwb3NpdGlvbiA9IC0xO1xuXG4gIGlmIChsaXN0ID09PSBsaXN0ZW5lciB8fFxuICAgICAgKGlzRnVuY3Rpb24obGlzdC5saXN0ZW5lcikgJiYgbGlzdC5saXN0ZW5lciA9PT0gbGlzdGVuZXIpKSB7XG4gICAgZGVsZXRlIHRoaXMuX2V2ZW50c1t0eXBlXTtcbiAgICBpZiAodGhpcy5fZXZlbnRzLnJlbW92ZUxpc3RlbmVyKVxuICAgICAgdGhpcy5lbWl0KCdyZW1vdmVMaXN0ZW5lcicsIHR5cGUsIGxpc3RlbmVyKTtcblxuICB9IGVsc2UgaWYgKGlzT2JqZWN0KGxpc3QpKSB7XG4gICAgZm9yIChpID0gbGVuZ3RoOyBpLS0gPiAwOykge1xuICAgICAgaWYgKGxpc3RbaV0gPT09IGxpc3RlbmVyIHx8XG4gICAgICAgICAgKGxpc3RbaV0ubGlzdGVuZXIgJiYgbGlzdFtpXS5saXN0ZW5lciA9PT0gbGlzdGVuZXIpKSB7XG4gICAgICAgIHBvc2l0aW9uID0gaTtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG4gICAgfVxuXG4gICAgaWYgKHBvc2l0aW9uIDwgMClcbiAgICAgIHJldHVybiB0aGlzO1xuXG4gICAgaWYgKGxpc3QubGVuZ3RoID09PSAxKSB7XG4gICAgICBsaXN0Lmxlbmd0aCA9IDA7XG4gICAgICBkZWxldGUgdGhpcy5fZXZlbnRzW3R5cGVdO1xuICAgIH0gZWxzZSB7XG4gICAgICBsaXN0LnNwbGljZShwb3NpdGlvbiwgMSk7XG4gICAgfVxuXG4gICAgaWYgKHRoaXMuX2V2ZW50cy5yZW1vdmVMaXN0ZW5lcilcbiAgICAgIHRoaXMuZW1pdCgncmVtb3ZlTGlzdGVuZXInLCB0eXBlLCBsaXN0ZW5lcik7XG4gIH1cblxuICByZXR1cm4gdGhpcztcbn07XG5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUucmVtb3ZlQWxsTGlzdGVuZXJzID0gZnVuY3Rpb24odHlwZSkge1xuICB2YXIga2V5LCBsaXN0ZW5lcnM7XG5cbiAgaWYgKCF0aGlzLl9ldmVudHMpXG4gICAgcmV0dXJuIHRoaXM7XG5cbiAgLy8gbm90IGxpc3RlbmluZyBmb3IgcmVtb3ZlTGlzdGVuZXIsIG5vIG5lZWQgdG8gZW1pdFxuICBpZiAoIXRoaXMuX2V2ZW50cy5yZW1vdmVMaXN0ZW5lcikge1xuICAgIGlmIChhcmd1bWVudHMubGVuZ3RoID09PSAwKVxuICAgICAgdGhpcy5fZXZlbnRzID0ge307XG4gICAgZWxzZSBpZiAodGhpcy5fZXZlbnRzW3R5cGVdKVxuICAgICAgZGVsZXRlIHRoaXMuX2V2ZW50c1t0eXBlXTtcbiAgICByZXR1cm4gdGhpcztcbiAgfVxuXG4gIC8vIGVtaXQgcmVtb3ZlTGlzdGVuZXIgZm9yIGFsbCBsaXN0ZW5lcnMgb24gYWxsIGV2ZW50c1xuICBpZiAoYXJndW1lbnRzLmxlbmd0aCA9PT0gMCkge1xuICAgIGZvciAoa2V5IGluIHRoaXMuX2V2ZW50cykge1xuICAgICAgaWYgKGtleSA9PT0gJ3JlbW92ZUxpc3RlbmVyJykgY29udGludWU7XG4gICAgICB0aGlzLnJlbW92ZUFsbExpc3RlbmVycyhrZXkpO1xuICAgIH1cbiAgICB0aGlzLnJlbW92ZUFsbExpc3RlbmVycygncmVtb3ZlTGlzdGVuZXInKTtcbiAgICB0aGlzLl9ldmVudHMgPSB7fTtcbiAgICByZXR1cm4gdGhpcztcbiAgfVxuXG4gIGxpc3RlbmVycyA9IHRoaXMuX2V2ZW50c1t0eXBlXTtcblxuICBpZiAoaXNGdW5jdGlvbihsaXN0ZW5lcnMpKSB7XG4gICAgdGhpcy5yZW1vdmVMaXN0ZW5lcih0eXBlLCBsaXN0ZW5lcnMpO1xuICB9IGVsc2Uge1xuICAgIC8vIExJRk8gb3JkZXJcbiAgICB3aGlsZSAobGlzdGVuZXJzLmxlbmd0aClcbiAgICAgIHRoaXMucmVtb3ZlTGlzdGVuZXIodHlwZSwgbGlzdGVuZXJzW2xpc3RlbmVycy5sZW5ndGggLSAxXSk7XG4gIH1cbiAgZGVsZXRlIHRoaXMuX2V2ZW50c1t0eXBlXTtcblxuICByZXR1cm4gdGhpcztcbn07XG5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUubGlzdGVuZXJzID0gZnVuY3Rpb24odHlwZSkge1xuICB2YXIgcmV0O1xuICBpZiAoIXRoaXMuX2V2ZW50cyB8fCAhdGhpcy5fZXZlbnRzW3R5cGVdKVxuICAgIHJldCA9IFtdO1xuICBlbHNlIGlmIChpc0Z1bmN0aW9uKHRoaXMuX2V2ZW50c1t0eXBlXSkpXG4gICAgcmV0ID0gW3RoaXMuX2V2ZW50c1t0eXBlXV07XG4gIGVsc2VcbiAgICByZXQgPSB0aGlzLl9ldmVudHNbdHlwZV0uc2xpY2UoKTtcbiAgcmV0dXJuIHJldDtcbn07XG5cbkV2ZW50RW1pdHRlci5saXN0ZW5lckNvdW50ID0gZnVuY3Rpb24oZW1pdHRlciwgdHlwZSkge1xuICB2YXIgcmV0O1xuICBpZiAoIWVtaXR0ZXIuX2V2ZW50cyB8fCAhZW1pdHRlci5fZXZlbnRzW3R5cGVdKVxuICAgIHJldCA9IDA7XG4gIGVsc2UgaWYgKGlzRnVuY3Rpb24oZW1pdHRlci5fZXZlbnRzW3R5cGVdKSlcbiAgICByZXQgPSAxO1xuICBlbHNlXG4gICAgcmV0ID0gZW1pdHRlci5fZXZlbnRzW3R5cGVdLmxlbmd0aDtcbiAgcmV0dXJuIHJldDtcbn07XG5cbmZ1bmN0aW9uIGlzRnVuY3Rpb24oYXJnKSB7XG4gIHJldHVybiB0eXBlb2YgYXJnID09PSAnZnVuY3Rpb24nO1xufVxuXG5mdW5jdGlvbiBpc051bWJlcihhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdudW1iZXInO1xufVxuXG5mdW5jdGlvbiBpc09iamVjdChhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdvYmplY3QnICYmIGFyZyAhPT0gbnVsbDtcbn1cblxuZnVuY3Rpb24gaXNVbmRlZmluZWQoYXJnKSB7XG4gIHJldHVybiBhcmcgPT09IHZvaWQgMDtcbn1cbiIsIid1c2Ugc3RyaWN0JztcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCcuL2lzLWltcGxlbWVudGVkJykoKSA/IFN5bWJvbCA6IHJlcXVpcmUoJy4vcG9seWZpbGwnKTtcbiIsIid1c2Ugc3RyaWN0JztcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiAoKSB7XG5cdHZhciBzeW1ib2w7XG5cdGlmICh0eXBlb2YgU3ltYm9sICE9PSAnZnVuY3Rpb24nKSByZXR1cm4gZmFsc2U7XG5cdHN5bWJvbCA9IFN5bWJvbCgndGVzdCBzeW1ib2wnKTtcblx0dHJ5IHtcblx0XHRpZiAoU3RyaW5nKHN5bWJvbCkgIT09ICdTeW1ib2wgKHRlc3Qgc3ltYm9sKScpIHJldHVybiBmYWxzZTtcblx0fSBjYXRjaCAoZSkgeyByZXR1cm4gZmFsc2U7IH1cblx0aWYgKHR5cGVvZiBTeW1ib2wuaXRlcmF0b3IgPT09ICdzeW1ib2wnKSByZXR1cm4gdHJ1ZTtcblxuXHQvLyBSZXR1cm4gJ3RydWUnIGZvciBwb2x5ZmlsbHNcblx0aWYgKHR5cGVvZiBTeW1ib2wuaXNDb25jYXRTcHJlYWRhYmxlICE9PSAnb2JqZWN0JykgcmV0dXJuIGZhbHNlO1xuXHRpZiAodHlwZW9mIFN5bWJvbC5pc1JlZ0V4cCAhPT0gJ29iamVjdCcpIHJldHVybiBmYWxzZTtcblx0aWYgKHR5cGVvZiBTeW1ib2wuaXRlcmF0b3IgIT09ICdvYmplY3QnKSByZXR1cm4gZmFsc2U7XG5cdGlmICh0eXBlb2YgU3ltYm9sLnRvUHJpbWl0aXZlICE9PSAnb2JqZWN0JykgcmV0dXJuIGZhbHNlO1xuXHRpZiAodHlwZW9mIFN5bWJvbC50b1N0cmluZ1RhZyAhPT0gJ29iamVjdCcpIHJldHVybiBmYWxzZTtcblx0aWYgKHR5cGVvZiBTeW1ib2wudW5zY29wYWJsZXMgIT09ICdvYmplY3QnKSByZXR1cm4gZmFsc2U7XG5cblx0cmV0dXJuIHRydWU7XG59O1xuIiwiJ3VzZSBzdHJpY3QnO1xuXG52YXIgYXNzaWduICAgICAgICA9IHJlcXVpcmUoJ2VzNS1leHQvb2JqZWN0L2Fzc2lnbicpXG4gICwgbm9ybWFsaXplT3B0cyA9IHJlcXVpcmUoJ2VzNS1leHQvb2JqZWN0L25vcm1hbGl6ZS1vcHRpb25zJylcbiAgLCBpc0NhbGxhYmxlICAgID0gcmVxdWlyZSgnZXM1LWV4dC9vYmplY3QvaXMtY2FsbGFibGUnKVxuICAsIGNvbnRhaW5zICAgICAgPSByZXF1aXJlKCdlczUtZXh0L3N0cmluZy8jL2NvbnRhaW5zJylcblxuICAsIGQ7XG5cbmQgPSBtb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIChkc2NyLCB2YWx1ZS8qLCBvcHRpb25zKi8pIHtcblx0dmFyIGMsIGUsIHcsIG9wdGlvbnMsIGRlc2M7XG5cdGlmICgoYXJndW1lbnRzLmxlbmd0aCA8IDIpIHx8ICh0eXBlb2YgZHNjciAhPT0gJ3N0cmluZycpKSB7XG5cdFx0b3B0aW9ucyA9IHZhbHVlO1xuXHRcdHZhbHVlID0gZHNjcjtcblx0XHRkc2NyID0gbnVsbDtcblx0fSBlbHNlIHtcblx0XHRvcHRpb25zID0gYXJndW1lbnRzWzJdO1xuXHR9XG5cdGlmIChkc2NyID09IG51bGwpIHtcblx0XHRjID0gdyA9IHRydWU7XG5cdFx0ZSA9IGZhbHNlO1xuXHR9IGVsc2Uge1xuXHRcdGMgPSBjb250YWlucy5jYWxsKGRzY3IsICdjJyk7XG5cdFx0ZSA9IGNvbnRhaW5zLmNhbGwoZHNjciwgJ2UnKTtcblx0XHR3ID0gY29udGFpbnMuY2FsbChkc2NyLCAndycpO1xuXHR9XG5cblx0ZGVzYyA9IHsgdmFsdWU6IHZhbHVlLCBjb25maWd1cmFibGU6IGMsIGVudW1lcmFibGU6IGUsIHdyaXRhYmxlOiB3IH07XG5cdHJldHVybiAhb3B0aW9ucyA/IGRlc2MgOiBhc3NpZ24obm9ybWFsaXplT3B0cyhvcHRpb25zKSwgZGVzYyk7XG59O1xuXG5kLmdzID0gZnVuY3Rpb24gKGRzY3IsIGdldCwgc2V0LyosIG9wdGlvbnMqLykge1xuXHR2YXIgYywgZSwgb3B0aW9ucywgZGVzYztcblx0aWYgKHR5cGVvZiBkc2NyICE9PSAnc3RyaW5nJykge1xuXHRcdG9wdGlvbnMgPSBzZXQ7XG5cdFx0c2V0ID0gZ2V0O1xuXHRcdGdldCA9IGRzY3I7XG5cdFx0ZHNjciA9IG51bGw7XG5cdH0gZWxzZSB7XG5cdFx0b3B0aW9ucyA9IGFyZ3VtZW50c1szXTtcblx0fVxuXHRpZiAoZ2V0ID09IG51bGwpIHtcblx0XHRnZXQgPSB1bmRlZmluZWQ7XG5cdH0gZWxzZSBpZiAoIWlzQ2FsbGFibGUoZ2V0KSkge1xuXHRcdG9wdGlvbnMgPSBnZXQ7XG5cdFx0Z2V0ID0gc2V0ID0gdW5kZWZpbmVkO1xuXHR9IGVsc2UgaWYgKHNldCA9PSBudWxsKSB7XG5cdFx0c2V0ID0gdW5kZWZpbmVkO1xuXHR9IGVsc2UgaWYgKCFpc0NhbGxhYmxlKHNldCkpIHtcblx0XHRvcHRpb25zID0gc2V0O1xuXHRcdHNldCA9IHVuZGVmaW5lZDtcblx0fVxuXHRpZiAoZHNjciA9PSBudWxsKSB7XG5cdFx0YyA9IHRydWU7XG5cdFx0ZSA9IGZhbHNlO1xuXHR9IGVsc2Uge1xuXHRcdGMgPSBjb250YWlucy5jYWxsKGRzY3IsICdjJyk7XG5cdFx0ZSA9IGNvbnRhaW5zLmNhbGwoZHNjciwgJ2UnKTtcblx0fVxuXG5cdGRlc2MgPSB7IGdldDogZ2V0LCBzZXQ6IHNldCwgY29uZmlndXJhYmxlOiBjLCBlbnVtZXJhYmxlOiBlIH07XG5cdHJldHVybiAhb3B0aW9ucyA/IGRlc2MgOiBhc3NpZ24obm9ybWFsaXplT3B0cyhvcHRpb25zKSwgZGVzYyk7XG59O1xuIiwiJ3VzZSBzdHJpY3QnO1xuXG5tb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoJy4vaXMtaW1wbGVtZW50ZWQnKSgpXG5cdD8gT2JqZWN0LmFzc2lnblxuXHQ6IHJlcXVpcmUoJy4vc2hpbScpO1xuIiwiJ3VzZSBzdHJpY3QnO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uICgpIHtcblx0dmFyIGFzc2lnbiA9IE9iamVjdC5hc3NpZ24sIG9iajtcblx0aWYgKHR5cGVvZiBhc3NpZ24gIT09ICdmdW5jdGlvbicpIHJldHVybiBmYWxzZTtcblx0b2JqID0geyBmb286ICdyYXonIH07XG5cdGFzc2lnbihvYmosIHsgYmFyOiAnZHdhJyB9LCB7IHRyenk6ICd0cnp5JyB9KTtcblx0cmV0dXJuIChvYmouZm9vICsgb2JqLmJhciArIG9iai50cnp5KSA9PT0gJ3JhemR3YXRyenknO1xufTtcbiIsIid1c2Ugc3RyaWN0JztcblxudmFyIGtleXMgID0gcmVxdWlyZSgnLi4va2V5cycpXG4gICwgdmFsdWUgPSByZXF1aXJlKCcuLi92YWxpZC12YWx1ZScpXG5cbiAgLCBtYXggPSBNYXRoLm1heDtcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiAoZGVzdCwgc3JjLyosIOKApnNyY24qLykge1xuXHR2YXIgZXJyb3IsIGksIGwgPSBtYXgoYXJndW1lbnRzLmxlbmd0aCwgMiksIGFzc2lnbjtcblx0ZGVzdCA9IE9iamVjdCh2YWx1ZShkZXN0KSk7XG5cdGFzc2lnbiA9IGZ1bmN0aW9uIChrZXkpIHtcblx0XHR0cnkgeyBkZXN0W2tleV0gPSBzcmNba2V5XTsgfSBjYXRjaCAoZSkge1xuXHRcdFx0aWYgKCFlcnJvcikgZXJyb3IgPSBlO1xuXHRcdH1cblx0fTtcblx0Zm9yIChpID0gMTsgaSA8IGw7ICsraSkge1xuXHRcdHNyYyA9IGFyZ3VtZW50c1tpXTtcblx0XHRrZXlzKHNyYykuZm9yRWFjaChhc3NpZ24pO1xuXHR9XG5cdGlmIChlcnJvciAhPT0gdW5kZWZpbmVkKSB0aHJvdyBlcnJvcjtcblx0cmV0dXJuIGRlc3Q7XG59O1xuIiwiLy8gRGVwcmVjYXRlZFxuXG4ndXNlIHN0cmljdCc7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gKG9iaikgeyByZXR1cm4gdHlwZW9mIG9iaiA9PT0gJ2Z1bmN0aW9uJzsgfTtcbiIsIid1c2Ugc3RyaWN0JztcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCcuL2lzLWltcGxlbWVudGVkJykoKVxuXHQ/IE9iamVjdC5rZXlzXG5cdDogcmVxdWlyZSgnLi9zaGltJyk7XG4iLCIndXNlIHN0cmljdCc7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gKCkge1xuXHR0cnkge1xuXHRcdE9iamVjdC5rZXlzKCdwcmltaXRpdmUnKTtcblx0XHRyZXR1cm4gdHJ1ZTtcblx0fSBjYXRjaCAoZSkgeyByZXR1cm4gZmFsc2U7IH1cbn07XG4iLCIndXNlIHN0cmljdCc7XG5cbnZhciBrZXlzID0gT2JqZWN0LmtleXM7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gKG9iamVjdCkge1xuXHRyZXR1cm4ga2V5cyhvYmplY3QgPT0gbnVsbCA/IG9iamVjdCA6IE9iamVjdChvYmplY3QpKTtcbn07XG4iLCIndXNlIHN0cmljdCc7XG5cbnZhciBhc3NpZ24gPSByZXF1aXJlKCcuL2Fzc2lnbicpXG5cbiAgLCBmb3JFYWNoID0gQXJyYXkucHJvdG90eXBlLmZvckVhY2hcbiAgLCBjcmVhdGUgPSBPYmplY3QuY3JlYXRlLCBnZXRQcm90b3R5cGVPZiA9IE9iamVjdC5nZXRQcm90b3R5cGVPZlxuXG4gICwgcHJvY2VzcztcblxucHJvY2VzcyA9IGZ1bmN0aW9uIChzcmMsIG9iaikge1xuXHR2YXIgcHJvdG8gPSBnZXRQcm90b3R5cGVPZihzcmMpO1xuXHRyZXR1cm4gYXNzaWduKHByb3RvID8gcHJvY2Vzcyhwcm90bywgb2JqKSA6IG9iaiwgc3JjKTtcbn07XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gKG9wdGlvbnMvKiwg4oCmb3B0aW9ucyovKSB7XG5cdHZhciByZXN1bHQgPSBjcmVhdGUobnVsbCk7XG5cdGZvckVhY2guY2FsbChhcmd1bWVudHMsIGZ1bmN0aW9uIChvcHRpb25zKSB7XG5cdFx0aWYgKG9wdGlvbnMgPT0gbnVsbCkgcmV0dXJuO1xuXHRcdHByb2Nlc3MoT2JqZWN0KG9wdGlvbnMpLCByZXN1bHQpO1xuXHR9KTtcblx0cmV0dXJuIHJlc3VsdDtcbn07XG4iLCIndXNlIHN0cmljdCc7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gKHZhbHVlKSB7XG5cdGlmICh2YWx1ZSA9PSBudWxsKSB0aHJvdyBuZXcgVHlwZUVycm9yKFwiQ2Fubm90IHVzZSBudWxsIG9yIHVuZGVmaW5lZFwiKTtcblx0cmV0dXJuIHZhbHVlO1xufTtcbiIsIid1c2Ugc3RyaWN0JztcblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCcuL2lzLWltcGxlbWVudGVkJykoKVxuXHQ/IFN0cmluZy5wcm90b3R5cGUuY29udGFpbnNcblx0OiByZXF1aXJlKCcuL3NoaW0nKTtcbiIsIid1c2Ugc3RyaWN0JztcblxudmFyIHN0ciA9ICdyYXpkd2F0cnp5JztcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiAoKSB7XG5cdGlmICh0eXBlb2Ygc3RyLmNvbnRhaW5zICE9PSAnZnVuY3Rpb24nKSByZXR1cm4gZmFsc2U7XG5cdHJldHVybiAoKHN0ci5jb250YWlucygnZHdhJykgPT09IHRydWUpICYmIChzdHIuY29udGFpbnMoJ2ZvbycpID09PSBmYWxzZSkpO1xufTtcbiIsIid1c2Ugc3RyaWN0JztcblxudmFyIGluZGV4T2YgPSBTdHJpbmcucHJvdG90eXBlLmluZGV4T2Y7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gKHNlYXJjaFN0cmluZy8qLCBwb3NpdGlvbiovKSB7XG5cdHJldHVybiBpbmRleE9mLmNhbGwodGhpcywgc2VhcmNoU3RyaW5nLCBhcmd1bWVudHNbMV0pID4gLTE7XG59O1xuIiwiJ3VzZSBzdHJpY3QnO1xuXG52YXIgZCA9IHJlcXVpcmUoJ2QnKVxuXG4gICwgY3JlYXRlID0gT2JqZWN0LmNyZWF0ZSwgZGVmaW5lUHJvcGVydGllcyA9IE9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzXG4gICwgZ2VuZXJhdGVOYW1lLCBTeW1ib2w7XG5cbmdlbmVyYXRlTmFtZSA9IChmdW5jdGlvbiAoKSB7XG5cdHZhciBjcmVhdGVkID0gY3JlYXRlKG51bGwpO1xuXHRyZXR1cm4gZnVuY3Rpb24gKGRlc2MpIHtcblx0XHR2YXIgcG9zdGZpeCA9IDA7XG5cdFx0d2hpbGUgKGNyZWF0ZWRbZGVzYyArIChwb3N0Zml4IHx8ICcnKV0pICsrcG9zdGZpeDtcblx0XHRkZXNjICs9IChwb3N0Zml4IHx8ICcnKTtcblx0XHRjcmVhdGVkW2Rlc2NdID0gdHJ1ZTtcblx0XHRyZXR1cm4gJ0BAJyArIGRlc2M7XG5cdH07XG59KCkpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IFN5bWJvbCA9IGZ1bmN0aW9uIChkZXNjcmlwdGlvbikge1xuXHR2YXIgc3ltYm9sO1xuXHRpZiAodGhpcyBpbnN0YW5jZW9mIFN5bWJvbCkge1xuXHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ1R5cGVFcnJvcjogU3ltYm9sIGlzIG5vdCBhIGNvbnN0cnVjdG9yJyk7XG5cdH1cblx0c3ltYm9sID0gY3JlYXRlKFN5bWJvbC5wcm90b3R5cGUpO1xuXHRkZXNjcmlwdGlvbiA9IChkZXNjcmlwdGlvbiA9PT0gdW5kZWZpbmVkID8gJycgOiBTdHJpbmcoZGVzY3JpcHRpb24pKTtcblx0cmV0dXJuIGRlZmluZVByb3BlcnRpZXMoc3ltYm9sLCB7XG5cdFx0X19kZXNjcmlwdGlvbl9fOiBkKCcnLCBkZXNjcmlwdGlvbiksXG5cdFx0X19uYW1lX186IGQoJycsIGdlbmVyYXRlTmFtZShkZXNjcmlwdGlvbikpXG5cdH0pO1xufTtcblxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoU3ltYm9sLCB7XG5cdGNyZWF0ZTogZCgnJywgU3ltYm9sKCdjcmVhdGUnKSksXG5cdGhhc0luc3RhbmNlOiBkKCcnLCBTeW1ib2woJ2hhc0luc3RhbmNlJykpLFxuXHRpc0NvbmNhdFNwcmVhZGFibGU6IGQoJycsIFN5bWJvbCgnaXNDb25jYXRTcHJlYWRhYmxlJykpLFxuXHRpc1JlZ0V4cDogZCgnJywgU3ltYm9sKCdpc1JlZ0V4cCcpKSxcblx0aXRlcmF0b3I6IGQoJycsIFN5bWJvbCgnaXRlcmF0b3InKSksXG5cdHRvUHJpbWl0aXZlOiBkKCcnLCBTeW1ib2woJ3RvUHJpbWl0aXZlJykpLFxuXHR0b1N0cmluZ1RhZzogZCgnJywgU3ltYm9sKCd0b1N0cmluZ1RhZycpKSxcblx0dW5zY29wYWJsZXM6IGQoJycsIFN5bWJvbCgndW5zY29wYWJsZXMnKSlcbn0pO1xuXG5kZWZpbmVQcm9wZXJ0aWVzKFN5bWJvbC5wcm90b3R5cGUsIHtcblx0cHJvcGVyVG9TdHJpbmc6IGQoZnVuY3Rpb24gKCkge1xuXHRcdHJldHVybiAnU3ltYm9sICgnICsgdGhpcy5fX2Rlc2NyaXB0aW9uX18gKyAnKSc7XG5cdH0pLFxuXHR0b1N0cmluZzogZCgnJywgZnVuY3Rpb24gKCkgeyByZXR1cm4gdGhpcy5fX25hbWVfXzsgfSlcbn0pO1xuT2JqZWN0LmRlZmluZVByb3BlcnR5KFN5bWJvbC5wcm90b3R5cGUsIFN5bWJvbC50b1ByaW1pdGl2ZSwgZCgnJyxcblx0ZnVuY3Rpb24gKGhpbnQpIHtcblx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKFwiQ29udmVyc2lvbiBvZiBzeW1ib2wgb2JqZWN0cyBpcyBub3QgYWxsb3dlZFwiKTtcblx0fSkpO1xuT2JqZWN0LmRlZmluZVByb3BlcnR5KFN5bWJvbC5wcm90b3R5cGUsIFN5bWJvbC50b1N0cmluZ1RhZywgZCgnYycsICdTeW1ib2wnKSk7XG4iLCJtb2R1bGUuZXhwb3J0cz17XG4gIFwidHlwZXNcIjoge1xuICAgIFwicm9vdFwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJyb290XCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZWNob1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInN0cmluZ1wiOiB7IFwiX2FyZ1wiOiAwLCBcInR5cGVcIjogXCJzdHJpbmdcIiB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwic3RyaW5nXCI6IHsgXCJfcmV0dmFsXCI6IFwic3RyaW5nXCIgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImxpc3RUYWJzXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHt9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjogeyBcIl9yZXR2YWxcIjogXCJ0YWJsaXN0XCIgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwicHJvdG9jb2xEZXNjcmlwdGlvblwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7fSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHsgXCJfcmV0dmFsXCI6IFwianNvblwiIH1cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHtcbiAgICAgICAgXCJ0YWJMaXN0Q2hhbmdlZFwiOiB7fVxuICAgICAgfVxuICAgIH0sXG4gICAgXCJ0YWJsaXN0XCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJkaWN0XCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwidGFibGlzdFwiLFxuICAgICAgXCJzcGVjaWFsaXphdGlvbnNcIjoge1xuICAgICAgICBcInNlbGVjdGVkXCI6IFwibnVtYmVyXCIsXG4gICAgICAgIFwidGFic1wiOiBcImFycmF5OnRhYlwiLFxuICAgICAgICBcInVybFwiOiBcInN0cmluZ1wiLFxuICAgICAgICBcImNvbnNvbGVBY3RvclwiOiBcImNvbnNvbGVcIixcbiAgICAgICAgXCJpbnNwZWN0b3JBY3RvclwiOiBcImluc3BlY3RvclwiLFxuICAgICAgICBcInN0eWxlU2hlZXRzQWN0b3JcIjogXCJzdHlsZXNoZWV0c1wiLFxuICAgICAgICBcInN0eWxlRWRpdG9yQWN0b3JcIjogXCJzdHlsZWVkaXRvclwiLFxuICAgICAgICBcIm1lbW9yeUFjdG9yXCI6IFwibWVtb3J5XCIsXG4gICAgICAgIFwiZXZlbnRMb29wTGFnQWN0b3JcIjogXCJldmVudExvb3BMYWdcIixcbiAgICAgICAgXCJwcmVmZXJlbmNlQWN0b3JcIjogXCJwcmVmZXJlbmNlXCIsXG4gICAgICAgIFwiZGV2aWNlQWN0b3JcIjogXCJkZXZpY2VcIixcblxuICAgICAgICBcInByb2ZpbGVyQWN0b3JcIjogXCJwcm9maWxlclwiLFxuICAgICAgICBcImNocm9tZURlYnVnZ2VyXCI6IFwiY2hyb21lRGVidWdnZXJcIixcbiAgICAgICAgXCJ3ZWJhcHBzQWN0b3JcIjogXCJ3ZWJhcHBzXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwidGFiXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJhY3RvclwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcInRhYlwiLFxuICAgICAgXCJmaWVsZHNcIjoge1xuICAgICAgICBcInRpdGxlXCI6IFwic3RyaW5nXCIsXG4gICAgICAgIFwidXJsXCI6IFwic3RyaW5nXCIsXG4gICAgICAgIFwib3V0ZXJXaW5kb3dJRFwiOiBcIm51bWJlclwiLFxuICAgICAgICBcImluc3BlY3RvckFjdG9yXCI6IFwiaW5zcGVjdG9yXCIsXG4gICAgICAgIFwiY2FsbFdhdGNoZXJBY3RvclwiOiBcImNhbGwtd2F0Y2hlclwiLFxuICAgICAgICBcImNhbnZhc0FjdG9yXCI6IFwiY2FudmFzXCIsXG4gICAgICAgIFwid2ViZ2xBY3RvclwiOiBcIndlYmdsXCIsXG4gICAgICAgIFwid2ViYXVkaW9BY3RvclwiOiBcIndlYmF1ZGlvXCIsXG4gICAgICAgIFwic3RvcmFnZUFjdG9yXCI6IFwic3RvcmFnZVwiLFxuICAgICAgICBcImdjbGlBY3RvclwiOiBcImdjbGlcIixcbiAgICAgICAgXCJtZW1vcnlBY3RvclwiOiBcIm1lbW9yeVwiLFxuICAgICAgICBcImV2ZW50TG9vcExhZ1wiOiBcImV2ZW50TG9vcExhZ1wiLFxuICAgICAgICBcInN0eWxlU2hlZXRzQWN0b3JcIjogXCJzdHlsZXNoZWV0c1wiLFxuICAgICAgICBcInN0eWxlRWRpdG9yQWN0b3JcIjogXCJzdHlsZWVkaXRvclwiLFxuXG4gICAgICAgIFwiY29uc29sZUFjdG9yXCI6IFwiY29uc29sZVwiLFxuICAgICAgICBcInRyYWNlQWN0b3JcIjogXCJ0cmFjZVwiXG4gICAgICB9LFxuICAgICAgXCJtZXRob2RzXCI6IFtcbiAgICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJhdHRhY2hcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge30sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7IFwiX3JldHZhbFwiOiBcImpzb25cIiB9XG4gICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge1xuICAgICAgICBcInRhYk5hdmlnYXRlZFwiOiB7XG4gICAgICAgICAgIFwidHlwZU5hbWVcIjogXCJ0YWJOYXZpZ2F0ZWRcIlxuICAgICAgICB9XG4gICAgICB9XG4gICAgfSxcbiAgICBcImNvbnNvbGVcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwiY29uc29sZVwiLFxuICAgICAgXCJtZXRob2RzXCI6IFtcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImV2YWx1YXRlSlNcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0ZXh0XCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJ1cmxcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcImJpbmRPYmplY3RBY3RvclwiOiB7XG4gICAgICAgICAgICAgIFwiX29wdGlvblwiOiAyLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJudWxsYWJsZTpzdHJpbmdcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwiZnJhbWVBY3RvclwiOiB7XG4gICAgICAgICAgICAgIFwiX29wdGlvblwiOiAyLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJudWxsYWJsZTpzdHJpbmdcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwic2VsZWN0ZWROb2RlQWN0b3JcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMixcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVsbGFibGU6c3RyaW5nXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiZXZhbHVhdGVqc3Jlc3BvbnNlXCJcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJldmFsdWF0ZWpzcmVzcG9uc2VcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImRpY3RcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJldmFsdWF0ZWpzcmVzcG9uc2VcIixcbiAgICAgIFwic3BlY2lhbGl6YXRpb25zXCI6IHtcbiAgICAgICAgXCJyZXN1bHRcIjogXCJvYmplY3RcIixcbiAgICAgICAgXCJleGNlcHRpb25cIjogXCJvYmplY3RcIixcbiAgICAgICAgXCJleGNlcHRpb25NZXNzYWdlXCI6IFwic3RyaW5nXCIsXG4gICAgICAgIFwiaW5wdXRcIjogXCJzdHJpbmdcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJvYmplY3RcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwib2JqZWN0XCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICAge1xuICAgICAgICAgICBcIm5hbWVcIjogXCJwcm9wZXJ0eVwiLFxuICAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgICBcIm5hbWVcIjoge1xuICAgICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgfSxcbiAgICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICAgIFwiZGVzY3JpcHRvclwiOiB7XG4gICAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwianNvblwiXG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgfVxuICAgICAgICAgfVxuICAgICAgXVxuICAgIH1cbiAgfVxufVxuIiwibW9kdWxlLmV4cG9ydHM9e1xuICBcInR5cGVzXCI6IHtcbiAgICBcImxvbmdzdHJhY3RvclwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJsb25nc3RyYWN0b3JcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJzdWJzdHJpbmdcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3Vic3RyaW5nXCIsXG4gICAgICAgICAgICBcInN0YXJ0XCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJlbmRcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJzdWJzdHJpbmdcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJwcmltaXRpdmVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInJlbGVhc2VcIixcbiAgICAgICAgICBcInJlbGVhc2VcIjogdHJ1ZSxcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwicmVsZWFzZVwiXG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJzdHlsZXNoZWV0XCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJhY3RvclwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcInN0eWxlc2hlZXRcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJ0b2dnbGVEaXNhYmxlZFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJ0b2dnbGVEaXNhYmxlZFwiXG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwiZGlzYWJsZWRcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJib29sZWFuXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRUZXh0XCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldFRleHRcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInRleHRcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJsb25nc3RyaW5nXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRPcmlnaW5hbFNvdXJjZXNcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0T3JpZ2luYWxTb3VyY2VzXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJvcmlnaW5hbFNvdXJjZXNcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJudWxsYWJsZTphcnJheTpvcmlnaW5hbHNvdXJjZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0T3JpZ2luYWxMb2NhdGlvblwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRPcmlnaW5hbExvY2F0aW9uXCIsXG4gICAgICAgICAgICBcImxpbmVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcImNvbHVtblwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJvcmlnaW5hbGxvY2F0aW9ucmVzcG9uc2VcIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInVwZGF0ZVwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJ1cGRhdGVcIixcbiAgICAgICAgICAgIFwidGV4dFwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwidHJhbnNpdGlvblwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHtcbiAgICAgICAgXCJwcm9wZXJ0eS1jaGFuZ2VcIjoge1xuICAgICAgICAgIFwidHlwZVwiOiBcInByb3BlcnR5Q2hhbmdlXCIsXG4gICAgICAgICAgXCJwcm9wZXJ0eVwiOiB7XG4gICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInZhbHVlXCI6IHtcbiAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwianNvblwiXG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICBcInN0eWxlLWFwcGxpZWRcIjoge1xuICAgICAgICAgIFwidHlwZVwiOiBcInN0eWxlQXBwbGllZFwiXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9LFxuICAgIFwib3JpZ2luYWxzb3VyY2VcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwib3JpZ2luYWxzb3VyY2VcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRUZXh0XCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldFRleHRcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInRleHRcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJsb25nc3RyaW5nXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJzdHlsZXNoZWV0c1wiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJzdHlsZXNoZWV0c1wiLFxuICAgICAgXCJtZXRob2RzXCI6IFtcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldFN0eWxlU2hlZXRzXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldFN0eWxlU2hlZXRzXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJzdHlsZVNoZWV0c1wiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImFycmF5OnN0eWxlc2hlZXRcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImFkZFN0eWxlU2hlZXRcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYWRkU3R5bGVTaGVldFwiLFxuICAgICAgICAgICAgXCJ0ZXh0XCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwic3R5bGVTaGVldFwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcInN0eWxlc2hlZXRcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHt9XG4gICAgfSxcbiAgICBcIm9yaWdpbmFsbG9jYXRpb25yZXNwb25zZVwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiZGljdFwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcIm9yaWdpbmFsbG9jYXRpb25yZXNwb25zZVwiLFxuICAgICAgXCJzcGVjaWFsaXphdGlvbnNcIjoge1xuICAgICAgICBcInNvdXJjZVwiOiBcInN0cmluZ1wiLFxuICAgICAgICBcImxpbmVcIjogXCJudW1iZXJcIixcbiAgICAgICAgXCJjb2x1bW5cIjogXCJudW1iZXJcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJkb21ub2RlXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJhY3RvclwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImRvbW5vZGVcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXROb2RlVmFsdWVcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0Tm9kZVZhbHVlXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJ2YWx1ZVwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImxvbmdzdHJpbmdcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInNldE5vZGVWYWx1ZVwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJzZXROb2RlVmFsdWVcIixcbiAgICAgICAgICAgIFwidmFsdWVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldEltYWdlRGF0YVwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRJbWFnZURhdGFcIixcbiAgICAgICAgICAgIFwibWF4RGltXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bGxhYmxlOm51bWJlclwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImltYWdlRGF0YVwiXG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwibW9kaWZ5QXR0cmlidXRlc1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJtb2RpZnlBdHRyaWJ1dGVzXCIsXG4gICAgICAgICAgICBcIm1vZGlmaWNhdGlvbnNcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYXJyYXk6anNvblwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJhcHBsaWVkc3R5bGVcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImRpY3RcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJhcHBsaWVkc3R5bGVcIixcbiAgICAgIFwic3BlY2lhbGl6YXRpb25zXCI6IHtcbiAgICAgICAgXCJydWxlXCI6IFwiZG9tc3R5bGVydWxlI2FjdG9yaWRcIixcbiAgICAgICAgXCJpbmhlcml0ZWRcIjogXCJudWxsYWJsZTpkb21ub2RlI2FjdG9yaWRcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJtYXRjaGVkc2VsZWN0b3JcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImRpY3RcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJtYXRjaGVkc2VsZWN0b3JcIixcbiAgICAgIFwic3BlY2lhbGl6YXRpb25zXCI6IHtcbiAgICAgICAgXCJydWxlXCI6IFwiZG9tc3R5bGVydWxlI2FjdG9yaWRcIixcbiAgICAgICAgXCJzZWxlY3RvclwiOiBcInN0cmluZ1wiLFxuICAgICAgICBcInZhbHVlXCI6IFwic3RyaW5nXCIsXG4gICAgICAgIFwic3RhdHVzXCI6IFwibnVtYmVyXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwibWF0Y2hlZHNlbGVjdG9ycmVzcG9uc2VcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImRpY3RcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJtYXRjaGVkc2VsZWN0b3JyZXNwb25zZVwiLFxuICAgICAgXCJzcGVjaWFsaXphdGlvbnNcIjoge1xuICAgICAgICBcInJ1bGVzXCI6IFwiYXJyYXk6ZG9tc3R5bGVydWxlXCIsXG4gICAgICAgIFwic2hlZXRzXCI6IFwiYXJyYXk6c3R5bGVzaGVldFwiLFxuICAgICAgICBcIm1hdGNoZWRcIjogXCJhcnJheTptYXRjaGVkc2VsZWN0b3JcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJhcHBsaWVkU3R5bGVzUmV0dXJuXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJkaWN0XCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwiYXBwbGllZFN0eWxlc1JldHVyblwiLFxuICAgICAgXCJzcGVjaWFsaXphdGlvbnNcIjoge1xuICAgICAgICBcImVudHJpZXNcIjogXCJhcnJheTphcHBsaWVkc3R5bGVcIixcbiAgICAgICAgXCJydWxlc1wiOiBcImFycmF5OmRvbXN0eWxlcnVsZVwiLFxuICAgICAgICBcInNoZWV0c1wiOiBcImFycmF5OnN0eWxlc2hlZXRcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJwYWdlc3R5bGVcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwicGFnZXN0eWxlXCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0Q29tcHV0ZWRcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0Q29tcHV0ZWRcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcIm1hcmtNYXRjaGVkXCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImJvb2xlYW5cIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwib25seU1hdGNoZWRcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJmaWx0ZXJcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJjb21wdXRlZFwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImpzb25cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldE1hdGNoZWRTZWxlY3RvcnNcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0TWF0Y2hlZFNlbGVjdG9yc1wiLFxuICAgICAgICAgICAgXCJub2RlXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImRvbW5vZGVcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwicHJvcGVydHlcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcImZpbHRlclwiOiB7XG4gICAgICAgICAgICAgIFwiX29wdGlvblwiOiAyLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJtYXRjaGVkc2VsZWN0b3JyZXNwb25zZVwiXG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0QXBwbGllZFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRBcHBsaWVkXCIsXG4gICAgICAgICAgICBcIm5vZGVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZG9tbm9kZVwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJpbmhlcml0ZWRcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJtYXRjaGVkU2VsZWN0b3JzXCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImJvb2xlYW5cIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwiZmlsdGVyXCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImFwcGxpZWRTdHlsZXNSZXR1cm5cIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldExheW91dFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRMYXlvdXRcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcImF1dG9NYXJnaW5zXCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImJvb2xlYW5cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJqc29uXCJcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJkb21zdHlsZXJ1bGVcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwiZG9tc3R5bGVydWxlXCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwibW9kaWZ5UHJvcGVydGllc1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJtb2RpZnlQcm9wZXJ0aWVzXCIsXG4gICAgICAgICAgICBcIm1vZGlmaWNhdGlvbnNcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYXJyYXk6anNvblwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwicnVsZVwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImRvbXN0eWxlcnVsZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge31cbiAgICB9LFxuICAgIFwiaGlnaGxpZ2h0ZXJcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwiaGlnaGxpZ2h0ZXJcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJzaG93Qm94TW9kZWxcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic2hvd0JveE1vZGVsXCIsXG4gICAgICAgICAgICBcIm5vZGVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZG9tbm9kZVwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJyZWdpb25cIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImhpZGVCb3hNb2RlbFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJoaWRlQm94TW9kZWxcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwicGlja1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJwaWNrXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImNhbmNlbFBpY2tcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiY2FuY2VsUGlja1wiXG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJpbWFnZURhdGFcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImRpY3RcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJpbWFnZURhdGFcIixcbiAgICAgIFwic3BlY2lhbGl6YXRpb25zXCI6IHtcbiAgICAgICAgXCJkYXRhXCI6IFwibnVsbGFibGU6bG9uZ3N0cmluZ1wiLFxuICAgICAgICBcInNpemVcIjogXCJqc29uXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiZGlzY29ubmVjdGVkTm9kZVwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiZGljdFwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImRpc2Nvbm5lY3RlZE5vZGVcIixcbiAgICAgIFwic3BlY2lhbGl6YXRpb25zXCI6IHtcbiAgICAgICAgXCJub2RlXCI6IFwiZG9tbm9kZVwiLFxuICAgICAgICBcIm5ld1BhcmVudHNcIjogXCJhcnJheTpkb21ub2RlXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiZGlzY29ubmVjdGVkTm9kZUFycmF5XCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJkaWN0XCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwiZGlzY29ubmVjdGVkTm9kZUFycmF5XCIsXG4gICAgICBcInNwZWNpYWxpemF0aW9uc1wiOiB7XG4gICAgICAgIFwibm9kZXNcIjogXCJhcnJheTpkb21ub2RlXCIsXG4gICAgICAgIFwibmV3UGFyZW50c1wiOiBcImFycmF5OmRvbW5vZGVcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJkb21tdXRhdGlvblwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiZGljdFwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImRvbW11dGF0aW9uXCIsXG4gICAgICBcInNwZWNpYWxpemF0aW9uc1wiOiB7fVxuICAgIH0sXG4gICAgXCJkb21ub2RlbGlzdFwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJkb21ub2RlbGlzdFwiLFxuICAgICAgXCJtZXRob2RzXCI6IFtcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcIml0ZW1cIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiaXRlbVwiLFxuICAgICAgICAgICAgXCJpdGVtXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImRpc2Nvbm5lY3RlZE5vZGVcIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcIml0ZW1zXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIml0ZW1zXCIsXG4gICAgICAgICAgICBcInN0YXJ0XCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bGxhYmxlOm51bWJlclwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJlbmRcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVsbGFibGU6bnVtYmVyXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiZGlzY29ubmVjdGVkTm9kZUFycmF5XCJcbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJyZWxlYXNlXCIsXG4gICAgICAgICAgXCJyZWxlYXNlXCI6IHRydWUsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInJlbGVhc2VcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge31cbiAgICB9LFxuICAgIFwiZG9tdHJhdmVyc2FsYXJyYXlcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImRpY3RcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJkb210cmF2ZXJzYWxhcnJheVwiLFxuICAgICAgXCJzcGVjaWFsaXphdGlvbnNcIjoge1xuICAgICAgICBcIm5vZGVzXCI6IFwiYXJyYXk6ZG9tbm9kZVwiXG4gICAgICB9XG4gICAgfSxcbiAgICBcImRvbXdhbGtlclwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJkb213YWxrZXJcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJyZWxlYXNlXCIsXG4gICAgICAgICAgXCJyZWxlYXNlXCI6IHRydWUsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInJlbGVhc2VcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwicGlja1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJwaWNrXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiZGlzY29ubmVjdGVkTm9kZVwiXG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiY2FuY2VsUGlja1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJjYW5jZWxQaWNrXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImhpZ2hsaWdodFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJoaWdobGlnaHRcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJudWxsYWJsZTpkb21ub2RlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImRvY3VtZW50XCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImRvY3VtZW50XCIsXG4gICAgICAgICAgICBcIm5vZGVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVsbGFibGU6ZG9tbm9kZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImRvbW5vZGVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImRvY3VtZW50RWxlbWVudFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJkb2N1bWVudEVsZW1lbnRcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJudWxsYWJsZTpkb21ub2RlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJub2RlXCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiZG9tbm9kZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwicGFyZW50c1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJwYXJlbnRzXCIsXG4gICAgICAgICAgICBcIm5vZGVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZG9tbm9kZVwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJzYW1lRG9jdW1lbnRcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJub2Rlc1wiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImFycmF5OmRvbW5vZGVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInJldGFpbk5vZGVcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwicmV0YWluTm9kZVwiLFxuICAgICAgICAgICAgXCJub2RlXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImRvbW5vZGVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwidW5yZXRhaW5Ob2RlXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInVucmV0YWluTm9kZVwiLFxuICAgICAgICAgICAgXCJub2RlXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImRvbW5vZGVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwicmVsZWFzZU5vZGVcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwicmVsZWFzZU5vZGVcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcImZvcmNlXCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJjaGlsZHJlblwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJjaGlsZHJlblwiLFxuICAgICAgICAgICAgXCJub2RlXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImRvbW5vZGVcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwibWF4Tm9kZXNcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcImNlbnRlclwiOiB7XG4gICAgICAgICAgICAgIFwiX29wdGlvblwiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcInN0YXJ0XCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImRvbW5vZGVcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwid2hhdFRvU2hvd1wiOiB7XG4gICAgICAgICAgICAgIFwiX29wdGlvblwiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJwcmltaXRpdmVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJkb210cmF2ZXJzYWxhcnJheVwiXG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwic2libGluZ3NcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic2libGluZ3NcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcIm1heE5vZGVzXCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJjZW50ZXJcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZG9tbm9kZVwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJzdGFydFwiOiB7XG4gICAgICAgICAgICAgIFwiX29wdGlvblwiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcIndoYXRUb1Nob3dcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiZG9tdHJhdmVyc2FsYXJyYXlcIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcIm5leHRTaWJsaW5nXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm5leHRTaWJsaW5nXCIsXG4gICAgICAgICAgICBcIm5vZGVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZG9tbm9kZVwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJ3aGF0VG9TaG93XCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcIm51bGxhYmxlOmRvbW5vZGVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInByZXZpb3VzU2libGluZ1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJwcmV2aW91c1NpYmxpbmdcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcIndoYXRUb1Nob3dcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJub2RlXCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwibnVsbGFibGU6ZG9tbm9kZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwicXVlcnlTZWxlY3RvclwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJxdWVyeVNlbGVjdG9yXCIsXG4gICAgICAgICAgICBcIm5vZGVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZG9tbm9kZVwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJzZWxlY3RvclwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJwcmltaXRpdmVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJkaXNjb25uZWN0ZWROb2RlXCJcbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJxdWVyeVNlbGVjdG9yQWxsXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInF1ZXJ5U2VsZWN0b3JBbGxcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcInNlbGVjdG9yXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwibGlzdFwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImRvbW5vZGVsaXN0XCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRTdWdnZXN0aW9uc0ZvclF1ZXJ5XCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldFN1Z2dlc3Rpb25zRm9yUXVlcnlcIixcbiAgICAgICAgICAgIFwicXVlcnlcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcImNvbXBsZXRpbmdcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcInNlbGVjdG9yU3RhdGVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMixcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJsaXN0XCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiYXJyYXk6YXJyYXk6c3RyaW5nXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJhZGRQc2V1ZG9DbGFzc0xvY2tcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYWRkUHNldWRvQ2xhc3NMb2NrXCIsXG4gICAgICAgICAgICBcIm5vZGVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZG9tbm9kZVwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJwc2V1ZG9DbGFzc1wiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJwcmltaXRpdmVcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwicGFyZW50c1wiOiB7XG4gICAgICAgICAgICAgIFwiX29wdGlvblwiOiAyLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJwcmltaXRpdmVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiaGlkZU5vZGVcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiaGlkZU5vZGVcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInVuaGlkZU5vZGVcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwidW5oaWRlTm9kZVwiLFxuICAgICAgICAgICAgXCJub2RlXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImRvbW5vZGVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwicmVtb3ZlUHNldWRvQ2xhc3NMb2NrXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInJlbW92ZVBzZXVkb0NsYXNzTG9ja1wiLFxuICAgICAgICAgICAgXCJub2RlXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImRvbW5vZGVcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwicHNldWRvQ2xhc3NcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcInBhcmVudHNcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMixcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImNsZWFyUHNldWRvQ2xhc3NMb2Nrc1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJjbGVhclBzZXVkb0NsYXNzTG9ja3NcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJudWxsYWJsZTpkb21ub2RlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImlubmVySFRNTFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJpbm5lckhUTUxcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJ2YWx1ZVwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImxvbmdzdHJpbmdcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcIm91dGVySFRNTFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJvdXRlckhUTUxcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJ2YWx1ZVwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImxvbmdzdHJpbmdcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInNldE91dGVySFRNTFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJzZXRPdXRlckhUTUxcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcInZhbHVlXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJyZW1vdmVOb2RlXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInJlbW92ZU5vZGVcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJuZXh0U2libGluZ1wiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcIm51bGxhYmxlOmRvbW5vZGVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImluc2VydEJlZm9yZVwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJpbnNlcnRCZWZvcmVcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcInBhcmVudFwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcInNpYmxpbmdcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMixcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVsbGFibGU6ZG9tbm9kZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRNdXRhdGlvbnNcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0TXV0YXRpb25zXCIsXG4gICAgICAgICAgICBcImNsZWFudXBcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJtdXRhdGlvbnNcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJhcnJheTpkb21tdXRhdGlvblwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiaXNJbkRPTVRyZWVcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiaXNJbkRPTVRyZWVcIixcbiAgICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJkb21ub2RlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJhdHRhY2hlZFwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImJvb2xlYW5cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldE5vZGVBY3RvckZyb21PYmplY3RBY3RvclwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXROb2RlQWN0b3JGcm9tT2JqZWN0QWN0b3JcIixcbiAgICAgICAgICAgIFwib2JqZWN0QWN0b3JJRFwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIm5vZGVGcm9udFwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcIm51bGxhYmxlOmRpc2Nvbm5lY3RlZE5vZGVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHtcbiAgICAgICAgXCJuZXctbXV0YXRpb25zXCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJuZXdNdXRhdGlvbnNcIlxuICAgICAgICB9LFxuICAgICAgICBcInBpY2tlci1ub2RlLXBpY2tlZFwiOiB7XG4gICAgICAgICAgXCJ0eXBlXCI6IFwicGlja2VyTm9kZVBpY2tlZFwiLFxuICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImRpc2Nvbm5lY3RlZE5vZGVcIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAgXCJwaWNrZXItbm9kZS1ob3ZlcmVkXCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJwaWNrZXJOb2RlSG92ZXJlZFwiLFxuICAgICAgICAgIFwibm9kZVwiOiB7XG4gICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImRpc2Nvbm5lY3RlZE5vZGVcIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAgXCJoaWdobGlnaHRlci1yZWFkeVwiOiB7XG4gICAgICAgICAgXCJ0eXBlXCI6IFwiaGlnaGxpZ2h0ZXItcmVhZHlcIlxuICAgICAgICB9LFxuICAgICAgICBcImhpZ2hsaWdodGVyLWhpZGVcIjoge1xuICAgICAgICAgIFwidHlwZVwiOiBcImhpZ2hsaWdodGVyLWhpZGVcIlxuICAgICAgICB9XG4gICAgICB9XG4gICAgfSxcbiAgICBcImluc3BlY3RvclwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJpbnNwZWN0b3JcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRXYWxrZXJcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0V2Fsa2VyXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJ3YWxrZXJcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJkb213YWxrZXJcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldFBhZ2VTdHlsZVwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRQYWdlU3R5bGVcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInBhZ2VTdHlsZVwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcInBhZ2VzdHlsZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0SGlnaGxpZ2h0ZXJcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0SGlnaGxpZ2h0ZXJcIixcbiAgICAgICAgICAgIFwiYXV0b2hpZGVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwiaGlnaGxpZ3RlclwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImhpZ2hsaWdodGVyXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRJbWFnZURhdGFGcm9tVVJMXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldEltYWdlRGF0YUZyb21VUkxcIixcbiAgICAgICAgICAgIFwidXJsXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJtYXhEaW1cIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVsbGFibGU6bnVtYmVyXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiaW1hZ2VEYXRhXCJcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJjYWxsLXN0YWNrLWl0ZW1cIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImRpY3RcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJjYWxsLXN0YWNrLWl0ZW1cIixcbiAgICAgIFwic3BlY2lhbGl6YXRpb25zXCI6IHtcbiAgICAgICAgXCJuYW1lXCI6IFwic3RyaW5nXCIsXG4gICAgICAgIFwiZmlsZVwiOiBcInN0cmluZ1wiLFxuICAgICAgICBcImxpbmVcIjogXCJudW1iZXJcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJjYWxsLWRldGFpbHNcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImRpY3RcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJjYWxsLWRldGFpbHNcIixcbiAgICAgIFwic3BlY2lhbGl6YXRpb25zXCI6IHtcbiAgICAgICAgXCJ0eXBlXCI6IFwibnVtYmVyXCIsXG4gICAgICAgIFwibmFtZVwiOiBcInN0cmluZ1wiLFxuICAgICAgICBcInN0YWNrXCI6IFwiYXJyYXk6Y2FsbC1zdGFjay1pdGVtXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiZnVuY3Rpb24tY2FsbFwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJmdW5jdGlvbi1jYWxsXCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0RGV0YWlsc1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXREZXRhaWxzXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJpbmZvXCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiY2FsbC1kZXRhaWxzXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJjYWxsLXdhdGNoZXJcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwiY2FsbC13YXRjaGVyXCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwic2V0dXBcIixcbiAgICAgICAgICBcIm9uZXdheVwiOiB0cnVlLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJzZXR1cFwiLFxuICAgICAgICAgICAgXCJ0cmFjZWRHbG9iYWxzXCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bGxhYmxlOmFycmF5OnN0cmluZ1wiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJ0cmFjZWRGdW5jdGlvbnNcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVsbGFibGU6YXJyYXk6c3RyaW5nXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcInN0YXJ0UmVjb3JkaW5nXCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImJvb2xlYW5cIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwicGVyZm9ybVJlbG9hZFwiOiB7XG4gICAgICAgICAgICAgIFwiX29wdGlvblwiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImZpbmFsaXplXCIsXG4gICAgICAgICAgXCJvbmV3YXlcIjogdHJ1ZSxcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZmluYWxpemVcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiaXNSZWNvcmRpbmdcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiaXNSZWNvcmRpbmdcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJib29sZWFuXCJcbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJyZXN1bWVSZWNvcmRpbmdcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwicmVzdW1lUmVjb3JkaW5nXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInBhdXNlUmVjb3JkaW5nXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInBhdXNlUmVjb3JkaW5nXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJjYWxsc1wiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImFycmF5OmZ1bmN0aW9uLWNhbGxcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImVyYXNlUmVjb3JkaW5nXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImVyYXNlUmVjb3JkaW5nXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHt9XG4gICAgfSxcbiAgICBcInNuYXBzaG90LWltYWdlXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJkaWN0XCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwic25hcHNob3QtaW1hZ2VcIixcbiAgICAgIFwic3BlY2lhbGl6YXRpb25zXCI6IHtcbiAgICAgICAgXCJpbmRleFwiOiBcIm51bWJlclwiLFxuICAgICAgICBcIndpZHRoXCI6IFwibnVtYmVyXCIsXG4gICAgICAgIFwiaGVpZ2h0XCI6IFwibnVtYmVyXCIsXG4gICAgICAgIFwiZmxpcHBlZFwiOiBcImJvb2xlYW5cIixcbiAgICAgICAgXCJwaXhlbHNcIjogXCJ1aW50MzItYXJyYXlcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJzbmFwc2hvdC1vdmVydmlld1wiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiZGljdFwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcInNuYXBzaG90LW92ZXJ2aWV3XCIsXG4gICAgICBcInNwZWNpYWxpemF0aW9uc1wiOiB7XG4gICAgICAgIFwiY2FsbHNcIjogXCJhcnJheTpmdW5jdGlvbi1jYWxsXCIsXG4gICAgICAgIFwidGh1bWJuYWlsc1wiOiBcImFycmF5OnNuYXBzaG90LWltYWdlXCIsXG4gICAgICAgIFwic2NyZWVuc2hvdFwiOiBcInNuYXBzaG90LWltYWdlXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiZnJhbWUtc25hcHNob3RcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwiZnJhbWUtc25hcHNob3RcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRPdmVydmlld1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRPdmVydmlld1wiXG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwib3ZlcnZpZXdcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJzbmFwc2hvdC1vdmVydmlld1wiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2VuZXJhdGVTY3JlZW5zaG90Rm9yXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdlbmVyYXRlU2NyZWVuc2hvdEZvclwiLFxuICAgICAgICAgICAgXCJjYWxsXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImZ1bmN0aW9uLWNhbGxcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInNjcmVlbnNob3RcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJzbmFwc2hvdC1pbWFnZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge31cbiAgICB9LFxuICAgIFwiY2FudmFzXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJhY3RvclwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImNhbnZhc1wiLFxuICAgICAgXCJtZXRob2RzXCI6IFtcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInNldHVwXCIsXG4gICAgICAgICAgXCJvbmV3YXlcIjogdHJ1ZSxcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic2V0dXBcIixcbiAgICAgICAgICAgIFwicmVsb2FkXCI6IHtcbiAgICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImJvb2xlYW5cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZmluYWxpemVcIixcbiAgICAgICAgICBcIm9uZXdheVwiOiB0cnVlLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJmaW5hbGl6ZVwiXG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJpc0luaXRpYWxpemVkXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImlzSW5pdGlhbGl6ZWRcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcImluaXRpYWxpemVkXCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiYm9vbGVhblwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwicmVjb3JkQW5pbWF0aW9uRnJhbWVcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwicmVjb3JkQW5pbWF0aW9uRnJhbWVcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInNuYXBzaG90XCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiZnJhbWUtc25hcHNob3RcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHt9XG4gICAgfSxcbiAgICBcImdsLXNoYWRlclwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJnbC1zaGFkZXJcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRUZXh0XCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldFRleHRcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInRleHRcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImNvbXBpbGVcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiY29tcGlsZVwiLFxuICAgICAgICAgICAgXCJ0ZXh0XCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwiZXJyb3JcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJudWxsYWJsZTpqc29uXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJnbC1wcm9ncmFtXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJhY3RvclwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImdsLXByb2dyYW1cIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRWZXJ0ZXhTaGFkZXJcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0VmVydGV4U2hhZGVyXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJzaGFkZXJcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJnbC1zaGFkZXJcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldEZyYWdtZW50U2hhZGVyXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldEZyYWdtZW50U2hhZGVyXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJzaGFkZXJcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJnbC1zaGFkZXJcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImhpZ2hsaWdodFwiLFxuICAgICAgICAgIFwib25ld2F5XCI6IHRydWUsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImhpZ2hsaWdodFwiLFxuICAgICAgICAgICAgXCJ0aW50XCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcImFycmF5Om51bWJlclwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJ1bmhpZ2hsaWdodFwiLFxuICAgICAgICAgIFwib25ld2F5XCI6IHRydWUsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInVuaGlnaGxpZ2h0XCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImJsYWNrYm94XCIsXG4gICAgICAgICAgXCJvbmV3YXlcIjogdHJ1ZSxcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYmxhY2tib3hcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwidW5ibGFja2JveFwiLFxuICAgICAgICAgIFwib25ld2F5XCI6IHRydWUsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInVuYmxhY2tib3hcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge31cbiAgICB9LFxuICAgIFwid2ViZ2xcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwid2ViZ2xcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJzZXR1cFwiLFxuICAgICAgICAgIFwib25ld2F5XCI6IHRydWUsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInNldHVwXCIsXG4gICAgICAgICAgICBcInJlbG9hZFwiOiB7XG4gICAgICAgICAgICAgIFwiX29wdGlvblwiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImZpbmFsaXplXCIsXG4gICAgICAgICAgXCJvbmV3YXlcIjogdHJ1ZSxcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZmluYWxpemVcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0UHJvZ3JhbXNcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0UHJvZ3JhbXNcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInByb2dyYW1zXCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwiYXJyYXk6Z2wtcHJvZ3JhbVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge1xuICAgICAgICBcInByb2dyYW0tbGlua2VkXCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJwcm9ncmFtTGlua2VkXCIsXG4gICAgICAgICAgXCJwcm9ncmFtXCI6IHtcbiAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2wtcHJvZ3JhbVwiXG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfSxcbiAgICBcImF1ZGlvbm9kZVwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJhdWRpb25vZGVcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRUeXBlXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldFR5cGVcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImlzU291cmNlXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImlzU291cmNlXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJzb3VyY2VcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJib29sZWFuXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJzZXRQYXJhbVwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJzZXRQYXJhbVwiLFxuICAgICAgICAgICAgXCJwYXJhbVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwidmFsdWVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVsbGFibGU6cHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJlcnJvclwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcIm51bGxhYmxlOmpzb25cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldFBhcmFtXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldFBhcmFtXCIsXG4gICAgICAgICAgICBcInBhcmFtXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwidGV4dFwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcIm51bGxhYmxlOnByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0UGFyYW1GbGFnc1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRQYXJhbUZsYWdzXCIsXG4gICAgICAgICAgICBcInBhcmFtXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwiZmxhZ3NcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJudWxsYWJsZTpwcmltaXRpdmVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldFBhcmFtc1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRQYXJhbXNcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInBhcmFtc1wiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImpzb25cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHt9XG4gICAgfSxcbiAgICBcIndlYmF1ZGlvXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJhY3RvclwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcIndlYmF1ZGlvXCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwic2V0dXBcIixcbiAgICAgICAgICBcIm9uZXdheVwiOiB0cnVlLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJzZXR1cFwiLFxuICAgICAgICAgICAgXCJyZWxvYWRcIjoge1xuICAgICAgICAgICAgICBcIl9vcHRpb25cIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYm9vbGVhblwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJmaW5hbGl6ZVwiLFxuICAgICAgICAgIFwib25ld2F5XCI6IHRydWUsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImZpbmFsaXplXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHtcbiAgICAgICAgXCJzdGFydC1jb250ZXh0XCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJzdGFydENvbnRleHRcIlxuICAgICAgICB9LFxuICAgICAgICBcImNvbm5lY3Qtbm9kZVwiOiB7XG4gICAgICAgICAgXCJ0eXBlXCI6IFwiY29ubmVjdE5vZGVcIixcbiAgICAgICAgICBcInNvdXJjZVwiOiB7XG4gICAgICAgICAgICBcIl9vcHRpb25cIjogMCxcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImF1ZGlvbm9kZVwiXG4gICAgICAgICAgfSxcbiAgICAgICAgICBcImRlc3RcIjoge1xuICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDAsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJhdWRpb25vZGVcIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAgXCJkaXNjb25uZWN0LW5vZGVcIjoge1xuICAgICAgICAgIFwidHlwZVwiOiBcImRpc2Nvbm5lY3ROb2RlXCIsXG4gICAgICAgICAgXCJzb3VyY2VcIjoge1xuICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJhdWRpb25vZGVcIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAgXCJjb25uZWN0LXBhcmFtXCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJjb25uZWN0UGFyYW1cIixcbiAgICAgICAgICBcInNvdXJjZVwiOiB7XG4gICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImF1ZGlvbm9kZVwiXG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInBhcmFtXCI6IHtcbiAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIFwiY2hhbmdlLXBhcmFtXCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJjaGFuZ2VQYXJhbVwiLFxuICAgICAgICAgIFwic291cmNlXCI6IHtcbiAgICAgICAgICAgIFwiX29wdGlvblwiOiAwLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYXVkaW9ub2RlXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicGFyYW1cIjoge1xuICAgICAgICAgICAgXCJfb3B0aW9uXCI6IDAsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJ2YWx1ZVwiOiB7XG4gICAgICAgICAgICBcIl9vcHRpb25cIjogMCxcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICBcImNyZWF0ZS1ub2RlXCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJjcmVhdGVOb2RlXCIsXG4gICAgICAgICAgXCJzb3VyY2VcIjoge1xuICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJhdWRpb25vZGVcIlxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0sXG4gICAgXCJvbGQtc3R5bGVzaGVldFwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJvbGQtc3R5bGVzaGVldFwiLFxuICAgICAgXCJtZXRob2RzXCI6IFtcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInRvZ2dsZURpc2FibGVkXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInRvZ2dsZURpc2FibGVkXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJkaXNhYmxlZFwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImJvb2xlYW5cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImZldGNoU291cmNlXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImZldGNoU291cmNlXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInVwZGF0ZVwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJ1cGRhdGVcIixcbiAgICAgICAgICAgIFwidGV4dFwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwidHJhbnNpdGlvblwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJib29sZWFuXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHtcbiAgICAgICAgXCJwcm9wZXJ0eS1jaGFuZ2VcIjoge1xuICAgICAgICAgIFwidHlwZVwiOiBcInByb3BlcnR5Q2hhbmdlXCIsXG4gICAgICAgICAgXCJwcm9wZXJ0eVwiOiB7XG4gICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInZhbHVlXCI6IHtcbiAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwianNvblwiXG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICBcInNvdXJjZS1sb2FkXCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJzb3VyY2VMb2FkXCIsXG4gICAgICAgICAgXCJzb3VyY2VcIjoge1xuICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAgXCJzdHlsZS1hcHBsaWVkXCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJzdHlsZUFwcGxpZWRcIlxuICAgICAgICB9XG4gICAgICB9XG4gICAgfSxcbiAgICBcInN0eWxlZWRpdG9yXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJhY3RvclwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcInN0eWxlZWRpdG9yXCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwibmV3RG9jdW1lbnRcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwibmV3RG9jdW1lbnRcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwibmV3U3R5bGVTaGVldFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJuZXdTdHlsZVNoZWV0XCIsXG4gICAgICAgICAgICBcInRleHRcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJzdHlsZVNoZWV0XCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwib2xkLXN0eWxlc2hlZXRcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHtcbiAgICAgICAgXCJkb2N1bWVudC1sb2FkXCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJkb2N1bWVudExvYWRcIixcbiAgICAgICAgICBcInN0eWxlU2hlZXRzXCI6IHtcbiAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiYXJyYXk6b2xkLXN0eWxlc2hlZXRcIlxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0sXG4gICAgXCJjb29raWVvYmplY3RcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImRpY3RcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJjb29raWVvYmplY3RcIixcbiAgICAgIFwic3BlY2lhbGl6YXRpb25zXCI6IHtcbiAgICAgICAgXCJuYW1lXCI6IFwic3RyaW5nXCIsXG4gICAgICAgIFwidmFsdWVcIjogXCJsb25nc3RyaW5nXCIsXG4gICAgICAgIFwicGF0aFwiOiBcIm51bGxhYmxlOnN0cmluZ1wiLFxuICAgICAgICBcImhvc3RcIjogXCJzdHJpbmdcIixcbiAgICAgICAgXCJpc0RvbWFpblwiOiBcImJvb2xlYW5cIixcbiAgICAgICAgXCJpc1NlY3VyZVwiOiBcImJvb2xlYW5cIixcbiAgICAgICAgXCJpc0h0dHBPbmx5XCI6IFwiYm9vbGVhblwiLFxuICAgICAgICBcImNyZWF0aW9uVGltZVwiOiBcIm51bWJlclwiLFxuICAgICAgICBcImxhc3RBY2Nlc3NlZFwiOiBcIm51bWJlclwiLFxuICAgICAgICBcImV4cGlyZXNcIjogXCJudW1iZXJcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJjb29raWVzdG9yZW9iamVjdFwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiZGljdFwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImNvb2tpZXN0b3Jlb2JqZWN0XCIsXG4gICAgICBcInNwZWNpYWxpemF0aW9uc1wiOiB7XG4gICAgICAgIFwidG90YWxcIjogXCJudW1iZXJcIixcbiAgICAgICAgXCJvZmZzZXRcIjogXCJudW1iZXJcIixcbiAgICAgICAgXCJkYXRhXCI6IFwiYXJyYXk6bnVsbGFibGU6Y29va2llb2JqZWN0XCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwic3RvcmFnZW9iamVjdFwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiZGljdFwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcInN0b3JhZ2VvYmplY3RcIixcbiAgICAgIFwic3BlY2lhbGl6YXRpb25zXCI6IHtcbiAgICAgICAgXCJuYW1lXCI6IFwic3RyaW5nXCIsXG4gICAgICAgIFwidmFsdWVcIjogXCJsb25nc3RyaW5nXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwic3RvcmFnZXN0b3Jlb2JqZWN0XCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJkaWN0XCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwic3RvcmFnZXN0b3Jlb2JqZWN0XCIsXG4gICAgICBcInNwZWNpYWxpemF0aW9uc1wiOiB7XG4gICAgICAgIFwidG90YWxcIjogXCJudW1iZXJcIixcbiAgICAgICAgXCJvZmZzZXRcIjogXCJudW1iZXJcIixcbiAgICAgICAgXCJkYXRhXCI6IFwiYXJyYXk6bnVsbGFibGU6c3RvcmFnZW9iamVjdFwiXG4gICAgICB9XG4gICAgfSxcbiAgICBcImlkYm9iamVjdFwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiZGljdFwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImlkYm9iamVjdFwiLFxuICAgICAgXCJzcGVjaWFsaXphdGlvbnNcIjoge1xuICAgICAgICBcIm5hbWVcIjogXCJudWxsYWJsZTpzdHJpbmdcIixcbiAgICAgICAgXCJkYlwiOiBcIm51bGxhYmxlOnN0cmluZ1wiLFxuICAgICAgICBcIm9iamVjdFN0b3JlXCI6IFwibnVsbGFibGU6c3RyaW5nXCIsXG4gICAgICAgIFwib3JpZ2luXCI6IFwibnVsbGFibGU6c3RyaW5nXCIsXG4gICAgICAgIFwidmVyc2lvblwiOiBcIm51bGxhYmxlOm51bWJlclwiLFxuICAgICAgICBcIm9iamVjdFN0b3Jlc1wiOiBcIm51bGxhYmxlOm51bWJlclwiLFxuICAgICAgICBcImtleVBhdGhcIjogXCJudWxsYWJsZTpzdHJpbmdcIixcbiAgICAgICAgXCJhdXRvSW5jcmVtZW50XCI6IFwibnVsbGFibGU6Ym9vbGVhblwiLFxuICAgICAgICBcImluZGV4ZXNcIjogXCJudWxsYWJsZTpzdHJpbmdcIixcbiAgICAgICAgXCJ2YWx1ZVwiOiBcIm51bGxhYmxlOmxvbmdzdHJpbmdcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJpZGJzdG9yZW9iamVjdFwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiZGljdFwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImlkYnN0b3Jlb2JqZWN0XCIsXG4gICAgICBcInNwZWNpYWxpemF0aW9uc1wiOiB7XG4gICAgICAgIFwidG90YWxcIjogXCJudW1iZXJcIixcbiAgICAgICAgXCJvZmZzZXRcIjogXCJudW1iZXJcIixcbiAgICAgICAgXCJkYXRhXCI6IFwiYXJyYXk6bnVsbGFibGU6aWRib2JqZWN0XCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwic3RvcmVVcGRhdGVPYmplY3RcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImRpY3RcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJzdG9yZVVwZGF0ZU9iamVjdFwiLFxuICAgICAgXCJzcGVjaWFsaXphdGlvbnNcIjoge1xuICAgICAgICBcImNoYW5nZWRcIjogXCJudWxsYWJsZTpqc29uXCIsXG4gICAgICAgIFwiZGVsZXRlZFwiOiBcIm51bGxhYmxlOmpzb25cIixcbiAgICAgICAgXCJhZGRlZFwiOiBcIm51bGxhYmxlOmpzb25cIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJjb29raWVzXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJhY3RvclwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImNvb2tpZXNcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRTdG9yZU9iamVjdHNcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0U3RvcmVPYmplY3RzXCIsXG4gICAgICAgICAgICBcImhvc3RcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcIm5hbWVzXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bGxhYmxlOmFycmF5OnN0cmluZ1wiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJvcHRpb25zXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDIsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bGxhYmxlOmpzb25cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJjb29raWVzdG9yZW9iamVjdFwiXG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge31cbiAgICB9LFxuICAgIFwibG9jYWxTdG9yYWdlXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJhY3RvclwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImxvY2FsU3RvcmFnZVwiLFxuICAgICAgXCJtZXRob2RzXCI6IFtcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldFN0b3JlT2JqZWN0c1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRTdG9yZU9iamVjdHNcIixcbiAgICAgICAgICAgIFwiaG9zdFwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJwcmltaXRpdmVcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwibmFtZXNcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVsbGFibGU6YXJyYXk6c3RyaW5nXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcIm9wdGlvbnNcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMixcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwibnVsbGFibGU6anNvblwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcInN0b3JhZ2VzdG9yZW9iamVjdFwiXG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge31cbiAgICB9LFxuICAgIFwic2Vzc2lvblN0b3JhZ2VcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwic2Vzc2lvblN0b3JhZ2VcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRTdG9yZU9iamVjdHNcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0U3RvcmVPYmplY3RzXCIsXG4gICAgICAgICAgICBcImhvc3RcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcIm5hbWVzXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bGxhYmxlOmFycmF5OnN0cmluZ1wiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJvcHRpb25zXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDIsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bGxhYmxlOmpzb25cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJzdG9yYWdlc3RvcmVvYmplY3RcIlxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHt9XG4gICAgfSxcbiAgICBcImluZGV4ZWREQlwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJpbmRleGVkREJcIixcbiAgICAgIFwibWV0aG9kc1wiOiBbXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRTdG9yZU9iamVjdHNcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0U3RvcmVPYmplY3RzXCIsXG4gICAgICAgICAgICBcImhvc3RcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcIm5hbWVzXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bGxhYmxlOmFycmF5OnN0cmluZ1wiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJvcHRpb25zXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDIsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bGxhYmxlOmpzb25cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJpZGJzdG9yZW9iamVjdFwiXG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge31cbiAgICB9LFxuICAgIFwic3RvcmVsaXN0XCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJkaWN0XCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwic3RvcmVsaXN0XCIsXG4gICAgICBcInNwZWNpYWxpemF0aW9uc1wiOiB7XG4gICAgICAgIFwiY29va2llc1wiOiBcImNvb2tpZXNcIixcbiAgICAgICAgXCJsb2NhbFN0b3JhZ2VcIjogXCJsb2NhbFN0b3JhZ2VcIixcbiAgICAgICAgXCJzZXNzaW9uU3RvcmFnZVwiOiBcInNlc3Npb25TdG9yYWdlXCIsXG4gICAgICAgIFwiaW5kZXhlZERCXCI6IFwiaW5kZXhlZERCXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwic3RvcmFnZVwiOiB7XG4gICAgICBcImNhdGVnb3J5XCI6IFwiYWN0b3JcIixcbiAgICAgIFwidHlwZU5hbWVcIjogXCJzdG9yYWdlXCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwibGlzdFN0b3Jlc1wiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJsaXN0U3RvcmVzXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwic3RvcmVsaXN0XCJcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7XG4gICAgICAgIFwic3RvcmVzLXVwZGF0ZVwiOiB7XG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RvcmVzVXBkYXRlXCIsXG4gICAgICAgICAgXCJkYXRhXCI6IHtcbiAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RvcmVVcGRhdGVPYmplY3RcIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAgXCJzdG9yZXMtY2xlYXJlZFwiOiB7XG4gICAgICAgICAgXCJ0eXBlXCI6IFwic3RvcmVzQ2xlYXJlZFwiLFxuICAgICAgICAgIFwiZGF0YVwiOiB7XG4gICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImpzb25cIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAgXCJzdG9yZXMtcmVsb2FkZWRcIjoge1xuICAgICAgICAgIFwidHlwZVwiOiBcInN0b3Jlc1JlbGFvZGVkXCIsXG4gICAgICAgICAgXCJkYXRhXCI6IHtcbiAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgXCJ0eXBlXCI6IFwianNvblwiXG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfSxcbiAgICBcImdjbGlcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwiZ2NsaVwiLFxuICAgICAgXCJtZXRob2RzXCI6IFtcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInNwZWNzXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInNwZWNzXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwianNvblwiXG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZXhlY3V0ZVwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJleGVjdXRlXCIsXG4gICAgICAgICAgICBcInR5cGVkXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImpzb25cIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInN0YXRlXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInN0YXRlXCIsXG4gICAgICAgICAgICBcInR5cGVkXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJzdGFydFwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwicmFua1wiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAyLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJudW1iZXJcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJqc29uXCJcbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJ0eXBlcGFyc2VcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwidHlwZXBhcnNlXCIsXG4gICAgICAgICAgICBcInR5cGVkXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJwYXJhbVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJqc29uXCJcbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJ0eXBlaW5jcmVtZW50XCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInR5cGVpbmNyZW1lbnRcIixcbiAgICAgICAgICAgIFwidHlwZWRcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcInBhcmFtXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwidHlwZWRlY3JlbWVudFwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJ0eXBlZGVjcmVtZW50XCIsXG4gICAgICAgICAgICBcInR5cGVkXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInN0cmluZ1wiXG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgXCJwYXJhbVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJzdHJpbmdcIlxuICAgICAgICAgIH1cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInNlbGVjdGlvbmluZm9cIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic2VsZWN0aW9uaW5mb1wiLFxuICAgICAgICAgICAgXCJ0eXBlZFwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwicGFyYW1cIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwic3RyaW5nXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcImFjdGlvblwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAxLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJzdHJpbmdcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJqc29uXCJcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJtZW1vcnlcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwibWVtb3J5XCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwibWVhc3VyZVwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJtZWFzdXJlXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwianNvblwiXG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge31cbiAgICB9LFxuICAgIFwiZXZlbnRMb29wTGFnXCI6IHtcbiAgICAgIFwiY2F0ZWdvcnlcIjogXCJhY3RvclwiLFxuICAgICAgXCJ0eXBlTmFtZVwiOiBcImV2ZW50TG9vcExhZ1wiLFxuICAgICAgXCJtZXRob2RzXCI6IFtcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInN0YXJ0XCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInN0YXJ0XCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJzdWNjZXNzXCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwibnVtYmVyXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJzdG9wXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInN0b3BcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7fVxuICAgICAgICB9XG4gICAgICBdLFxuICAgICAgXCJldmVudHNcIjoge1xuICAgICAgICBcImV2ZW50LWxvb3AtbGFnXCI6IHtcbiAgICAgICAgICBcInR5cGVcIjogXCJldmVudC1sb29wLWxhZ1wiLFxuICAgICAgICAgIFwidGltZVwiOiB7XG4gICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgIFwidHlwZVwiOiBcIm51bWJlclwiXG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfSxcbiAgICBcInByZWZlcmVuY2VcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwicHJlZmVyZW5jZVwiLFxuICAgICAgXCJtZXRob2RzXCI6IFtcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImdldEJvb2xQcmVmXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldEJvb2xQcmVmXCIsXG4gICAgICAgICAgICBcInZhbHVlXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwidmFsdWVcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJib29sZWFuXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRDaGFyUHJlZlwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRDaGFyUHJlZlwiLFxuICAgICAgICAgICAgXCJ2YWx1ZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJwcmltaXRpdmVcIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInZhbHVlXCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwic3RyaW5nXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJnZXRJbnRQcmVmXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldEludFByZWZcIixcbiAgICAgICAgICAgIFwidmFsdWVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJ2YWx1ZVwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcIm51bWJlclwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0QWxsUHJlZnNcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0QWxsUHJlZnNcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInZhbHVlXCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwianNvblwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwic2V0Qm9vbFByZWZcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwic2V0Qm9vbFByZWZcIixcbiAgICAgICAgICAgIFwibmFtZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJwcmltaXRpdmVcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwidmFsdWVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcInNldENoYXJQcmVmXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInNldENoYXJQcmVmXCIsXG4gICAgICAgICAgICBcIm5hbWVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMCxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBcInZhbHVlXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDEsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJzZXRJbnRQcmVmXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInNldEludFByZWZcIixcbiAgICAgICAgICAgIFwibmFtZVwiOiB7XG4gICAgICAgICAgICAgIFwiX2FyZ1wiOiAwLFxuICAgICAgICAgICAgICBcInR5cGVcIjogXCJwcmltaXRpdmVcIlxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIFwidmFsdWVcIjoge1xuICAgICAgICAgICAgICBcIl9hcmdcIjogMSxcbiAgICAgICAgICAgICAgXCJ0eXBlXCI6IFwicHJpbWl0aXZlXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge31cbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIFwibmFtZVwiOiBcImNsZWFyVXNlclByZWZcIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiY2xlYXJVc2VyUHJlZlwiLFxuICAgICAgICAgICAgXCJuYW1lXCI6IHtcbiAgICAgICAgICAgICAgXCJfYXJnXCI6IDAsXG4gICAgICAgICAgICAgIFwidHlwZVwiOiBcInByaW1pdGl2ZVwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHt9XG4gICAgICAgIH1cbiAgICAgIF0sXG4gICAgICBcImV2ZW50c1wiOiB7fVxuICAgIH0sXG4gICAgXCJkZXZpY2VcIjoge1xuICAgICAgXCJjYXRlZ29yeVwiOiBcImFjdG9yXCIsXG4gICAgICBcInR5cGVOYW1lXCI6IFwiZGV2aWNlXCIsXG4gICAgICBcIm1ldGhvZHNcIjogW1xuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0RGVzY3JpcHRpb25cIixcbiAgICAgICAgICBcInJlcXVlc3RcIjoge1xuICAgICAgICAgICAgXCJ0eXBlXCI6IFwiZ2V0RGVzY3JpcHRpb25cIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInZhbHVlXCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwianNvblwiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0V2FsbHBhcGVyXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcImdldFdhbGxwYXBlclwiXG4gICAgICAgICAgfSxcbiAgICAgICAgICBcInJlc3BvbnNlXCI6IHtcbiAgICAgICAgICAgIFwidmFsdWVcIjoge1xuICAgICAgICAgICAgICBcIl9yZXR2YWxcIjogXCJsb25nc3RyaW5nXCJcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH0sXG4gICAgICAgIHtcbiAgICAgICAgICBcIm5hbWVcIjogXCJzY3JlZW5zaG90VG9EYXRhVVJMXCIsXG4gICAgICAgICAgXCJyZXF1ZXN0XCI6IHtcbiAgICAgICAgICAgIFwidHlwZVwiOiBcInNjcmVlbnNob3RUb0RhdGFVUkxcIlxuICAgICAgICAgIH0sXG4gICAgICAgICAgXCJyZXNwb25zZVwiOiB7XG4gICAgICAgICAgICBcInZhbHVlXCI6IHtcbiAgICAgICAgICAgICAgXCJfcmV0dmFsXCI6IFwibG9uZ3N0cmluZ1wiXG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgXCJuYW1lXCI6IFwiZ2V0UmF3UGVybWlzc2lvbnNUYWJsZVwiLFxuICAgICAgICAgIFwicmVxdWVzdFwiOiB7XG4gICAgICAgICAgICBcInR5cGVcIjogXCJnZXRSYXdQZXJtaXNzaW9uc1RhYmxlXCJcbiAgICAgICAgICB9LFxuICAgICAgICAgIFwicmVzcG9uc2VcIjoge1xuICAgICAgICAgICAgXCJ2YWx1ZVwiOiB7XG4gICAgICAgICAgICAgIFwiX3JldHZhbFwiOiBcImpzb25cIlxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgXSxcbiAgICAgIFwiZXZlbnRzXCI6IHt9XG4gICAgfVxuICB9LFxuICBcImZyb21cIjogXCJyb290XCJcbn1cbiIsIlwidXNlIHN0cmljdFwiO1xuXG52YXIgQ2xhc3MgPSByZXF1aXJlKFwiLi9jbGFzc1wiKS5DbGFzcztcbnZhciB1dGlsID0gcmVxdWlyZShcIi4vdXRpbFwiKTtcbnZhciBrZXlzID0gdXRpbC5rZXlzO1xudmFyIHZhbHVlcyA9IHV0aWwudmFsdWVzO1xudmFyIHBhaXJzID0gdXRpbC5wYWlycztcbnZhciBxdWVyeSA9IHV0aWwucXVlcnk7XG52YXIgZmluZFBhdGggPSB1dGlsLmZpbmRQYXRoO1xudmFyIEV2ZW50VGFyZ2V0ID0gcmVxdWlyZShcIi4vZXZlbnRcIikuRXZlbnRUYXJnZXQ7XG5cbnZhciBUeXBlU3lzdGVtID0gQ2xhc3Moe1xuICBjb25zdHJ1Y3RvcjogZnVuY3Rpb24oY2xpZW50KSB7XG4gICAgdmFyIHR5cGVzID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcbiAgICB2YXIgc3BlY2lmaWNhdGlvbiA9IE9iamVjdC5jcmVhdGUobnVsbCk7XG5cbiAgICB0aGlzLnNwZWNpZmljYXRpb24gPSBzcGVjaWZpY2F0aW9uO1xuICAgIHRoaXMudHlwZXMgPSB0eXBlcztcblxuICAgIHZhciB0eXBlRm9yID0gZnVuY3Rpb24gdHlwZUZvcih0eXBlTmFtZSkge1xuICAgICAgdHlwZU5hbWUgPSB0eXBlTmFtZSB8fCBcInByaW1pdGl2ZVwiO1xuICAgICAgaWYgKCF0eXBlc1t0eXBlTmFtZV0pIHtcbiAgICAgICAgZGVmaW5lVHlwZSh0eXBlTmFtZSk7XG4gICAgICB9XG5cbiAgICAgIHJldHVybiB0eXBlc1t0eXBlTmFtZV07XG4gICAgfTtcbiAgICB0aGlzLnR5cGVGb3IgPSB0eXBlRm9yO1xuXG4gICAgdmFyIGRlZmluZVR5cGUgPSBmdW5jdGlvbihkZXNjcmlwdG9yKSB7XG4gICAgICB2YXIgdHlwZSA9IHZvaWQoMCk7XG4gICAgICBpZiAodHlwZW9mKGRlc2NyaXB0b3IpID09PSBcInN0cmluZ1wiKSB7XG4gICAgICAgIGlmIChkZXNjcmlwdG9yLmluZGV4T2YoXCI6XCIpID4gMClcbiAgICAgICAgICB0eXBlID0gbWFrZUNvbXBvdW5kVHlwZShkZXNjcmlwdG9yKTtcbiAgICAgICAgZWxzZSBpZiAoZGVzY3JpcHRvci5pbmRleE9mKFwiI1wiKSA+IDApXG4gICAgICAgICAgdHlwZSA9IG5ldyBBY3RvckRldGFpbChkZXNjcmlwdG9yKTtcbiAgICAgICAgICBlbHNlIGlmIChzcGVjaWZpY2F0aW9uW2Rlc2NyaXB0b3JdKVxuICAgICAgICAgICAgdHlwZSA9IG1ha2VDYXRlZ29yeVR5cGUoc3BlY2lmaWNhdGlvbltkZXNjcmlwdG9yXSk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0eXBlID0gbWFrZUNhdGVnb3J5VHlwZShkZXNjcmlwdG9yKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHR5cGUpXG4gICAgICAgIHR5cGVzW3R5cGUubmFtZV0gPSB0eXBlO1xuICAgICAgZWxzZVxuICAgICAgICB0aHJvdyBUeXBlRXJyb3IoXCJJbnZhbGlkIHR5cGU6IFwiICsgZGVzY3JpcHRvcik7XG4gICAgfTtcbiAgICB0aGlzLmRlZmluZVR5cGUgPSBkZWZpbmVUeXBlO1xuXG5cbiAgICB2YXIgbWFrZUNvbXBvdW5kVHlwZSA9IGZ1bmN0aW9uKG5hbWUpIHtcbiAgICAgIHZhciBpbmRleCA9IG5hbWUuaW5kZXhPZihcIjpcIik7XG4gICAgICB2YXIgYmFzZVR5cGUgPSBuYW1lLnNsaWNlKDAsIGluZGV4KTtcbiAgICAgIHZhciBzdWJUeXBlID0gbmFtZS5zbGljZShpbmRleCArIDEpO1xuXG4gICAgICByZXR1cm4gYmFzZVR5cGUgPT09IFwiYXJyYXlcIiA/IG5ldyBBcnJheU9mKHN1YlR5cGUpIDpcbiAgICAgIGJhc2VUeXBlID09PSBcIm51bGxhYmxlXCIgPyBuZXcgTWF5YmUoc3ViVHlwZSkgOlxuICAgICAgbnVsbDtcbiAgICB9O1xuXG4gICAgdmFyIG1ha2VDYXRlZ29yeVR5cGUgPSBmdW5jdGlvbihkZXNjcmlwdG9yKSB7XG4gICAgICB2YXIgY2F0ZWdvcnkgPSBkZXNjcmlwdG9yLmNhdGVnb3J5O1xuICAgICAgcmV0dXJuIGNhdGVnb3J5ID09PSBcImRpY3RcIiA/IG5ldyBEaWN0aW9uYXJ5KGRlc2NyaXB0b3IpIDpcbiAgICAgIGNhdGVnb3J5ID09PSBcImFjdG9yXCIgPyBuZXcgQWN0b3IoZGVzY3JpcHRvcikgOlxuICAgICAgbnVsbDtcbiAgICB9O1xuXG4gICAgdmFyIHJlYWQgPSBmdW5jdGlvbihpbnB1dCwgY29udGV4dCwgdHlwZU5hbWUpIHtcbiAgICAgIHJldHVybiB0eXBlRm9yKHR5cGVOYW1lKS5yZWFkKGlucHV0LCBjb250ZXh0KTtcbiAgICB9XG4gICAgdGhpcy5yZWFkID0gcmVhZDtcblxuICAgIHZhciB3cml0ZSA9IGZ1bmN0aW9uKGlucHV0LCBjb250ZXh0LCB0eXBlTmFtZSkge1xuICAgICAgcmV0dXJuIHR5cGVGb3IodHlwZU5hbWUpLndyaXRlKGlucHV0KTtcbiAgICB9O1xuICAgIHRoaXMud3JpdGUgPSB3cml0ZTtcblxuXG4gICAgdmFyIFR5cGUgPSBDbGFzcyh7XG4gICAgICBjb25zdHJ1Y3RvcjogZnVuY3Rpb24oKSB7XG4gICAgICB9LFxuICAgICAgZ2V0IG5hbWUoKSB7XG4gICAgICAgIHJldHVybiB0aGlzLmNhdGVnb3J5ID8gdGhpcy5jYXRlZ29yeSArIFwiOlwiICsgdGhpcy50eXBlIDpcbiAgICAgICAgdGhpcy50eXBlO1xuICAgICAgfSxcbiAgICAgIHJlYWQ6IGZ1bmN0aW9uKGlucHV0LCBjb250ZXh0KSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoXCJgVHlwZWAgc3ViY2xhc3MgbXVzdCBpbXBsZW1lbnQgYHJlYWRgXCIpO1xuICAgICAgfSxcbiAgICAgIHdyaXRlOiBmdW5jdGlvbihpbnB1dCwgY29udGV4dCkge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKFwiYFR5cGVgIHN1YmNsYXNzIG11c3QgaW1wbGVtZW50IGB3cml0ZWBcIik7XG4gICAgICB9XG4gICAgfSk7XG5cbiAgICB2YXIgUHJpbWl0dmUgPSBDbGFzcyh7XG4gICAgICBleHRlbmRzOiBUeXBlLFxuICAgICAgY29uc3R1Y3RvcjogZnVuY3Rpb24odHlwZSkge1xuICAgICAgICB0aGlzLnR5cGUgPSB0eXBlO1xuICAgICAgfSxcbiAgICAgIHJlYWQ6IGZ1bmN0aW9uKGlucHV0LCBjb250ZXh0KSB7XG4gICAgICAgIHJldHVybiBpbnB1dDtcbiAgICAgIH0sXG4gICAgICB3cml0ZTogZnVuY3Rpb24oaW5wdXQsIGNvbnRleHQpIHtcbiAgICAgICAgcmV0dXJuIGlucHV0O1xuICAgICAgfVxuICAgIH0pO1xuXG4gICAgdmFyIE1heWJlID0gQ2xhc3Moe1xuICAgICAgZXh0ZW5kczogVHlwZSxcbiAgICAgIGNhdGVnb3J5OiBcIm51bGxhYmxlXCIsXG4gICAgICBjb25zdHJ1Y3RvcjogZnVuY3Rpb24odHlwZSkge1xuICAgICAgICB0aGlzLnR5cGUgPSB0eXBlO1xuICAgICAgfSxcbiAgICAgIHJlYWQ6IGZ1bmN0aW9uKGlucHV0LCBjb250ZXh0KSB7XG4gICAgICAgIHJldHVybiBpbnB1dCA9PT0gbnVsbCA/IG51bGwgOlxuICAgICAgICBpbnB1dCA9PT0gdm9pZCgwKSA/IHZvaWQoMCkgOlxuICAgICAgICByZWFkKGlucHV0LCBjb250ZXh0LCB0aGlzLnR5cGUpO1xuICAgICAgfSxcbiAgICAgIHdyaXRlOiBmdW5jdGlvbihpbnB1dCwgY29udGV4dCkge1xuICAgICAgICByZXR1cm4gaW5wdXQgPT09IG51bGwgPyBudWxsIDpcbiAgICAgICAgaW5wdXQgPT09IHZvaWQoMCkgPyB2b2lkKDApIDpcbiAgICAgICAgd3JpdGUoaW5wdXQsIGNvbnRleHQsIHRoaXMudHlwZSk7XG4gICAgICB9XG4gICAgfSk7XG5cbiAgICB2YXIgQXJyYXlPZiA9IENsYXNzKHtcbiAgICAgIGV4dGVuZHM6IFR5cGUsXG4gICAgICBjYXRlZ29yeTogXCJhcnJheVwiLFxuICAgICAgY29uc3RydWN0b3I6IGZ1bmN0aW9uKHR5cGUpIHtcbiAgICAgICAgdGhpcy50eXBlID0gdHlwZTtcbiAgICAgIH0sXG4gICAgICByZWFkOiBmdW5jdGlvbihpbnB1dCwgY29udGV4dCkge1xuICAgICAgICB2YXIgdHlwZSA9IHRoaXMudHlwZTtcbiAgICAgICAgcmV0dXJuIGlucHV0Lm1hcChmdW5jdGlvbigkKSB7IHJldHVybiByZWFkKCQsIGNvbnRleHQsIHR5cGUpIH0pO1xuICAgICAgfSxcbiAgICAgIHdyaXRlOiBmdW5jdGlvbihpbnB1dCwgY29udGV4dCkge1xuICAgICAgICB2YXIgdHlwZSA9IHRoaXMudHlwZTtcbiAgICAgICAgcmV0dXJuIGlucHV0Lm1hcChmdW5jdGlvbigkKSB7IHJldHVybiB3cml0ZSgkLCBjb250ZXh0LCB0eXBlKSB9KTtcbiAgICAgIH1cbiAgICB9KTtcblxuICAgIHZhciBtYWtlRmllbGQgPSBmdW5jdGlvbiBtYWtlRmllbGQobmFtZSwgdHlwZSkge1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAgZW51bWVyYWJsZTogdHJ1ZSxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlLFxuICAgICAgICBnZXQ6IGZ1bmN0aW9uKCkge1xuICAgICAgICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0aGlzLCBuYW1lLCB7XG4gICAgICAgICAgICBjb25maWd1cmFibGU6IGZhbHNlLFxuICAgICAgICAgICAgdmFsdWU6IHJlYWQodGhpcy5zdGF0ZVtuYW1lXSwgdGhpcy5jb250ZXh0LCB0eXBlKVxuICAgICAgICAgIH0pO1xuICAgICAgICAgIHJldHVybiB0aGlzW25hbWVdO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfTtcblxuICAgIHZhciBtYWtlRmllbGRzID0gZnVuY3Rpb24oZGVzY3JpcHRvcikge1xuICAgICAgcmV0dXJuIHBhaXJzKGRlc2NyaXB0b3IpLnJlZHVjZShmdW5jdGlvbihmaWVsZHMsIHBhaXIpIHtcbiAgICAgICAgdmFyIG5hbWUgPSBwYWlyWzBdLCB0eXBlID0gcGFpclsxXTtcbiAgICAgICAgZmllbGRzW25hbWVdID0gbWFrZUZpZWxkKG5hbWUsIHR5cGUpO1xuICAgICAgICByZXR1cm4gZmllbGRzO1xuICAgICAgfSwge30pO1xuICAgIH1cblxuICAgIHZhciBEaWN0aW9uYXJ5VHlwZSA9IENsYXNzKHt9KTtcblxuICAgIHZhciBEaWN0aW9uYXJ5ID0gQ2xhc3Moe1xuICAgICAgZXh0ZW5kczogVHlwZSxcbiAgICAgIGNhdGVnb3J5OiBcImRpY3RcIixcbiAgICAgIGdldCBuYW1lKCkgeyByZXR1cm4gdGhpcy50eXBlOyB9LFxuICAgICAgY29uc3RydWN0b3I6IGZ1bmN0aW9uKGRlc2NyaXB0b3IpIHtcbiAgICAgICAgdGhpcy50eXBlID0gZGVzY3JpcHRvci50eXBlTmFtZTtcbiAgICAgICAgdGhpcy50eXBlcyA9IGRlc2NyaXB0b3Iuc3BlY2lhbGl6YXRpb25zO1xuXG4gICAgICAgIHZhciBwcm90byA9IE9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKHtcbiAgICAgICAgICBleHRlbmRzOiBEaWN0aW9uYXJ5VHlwZSxcbiAgICAgICAgICBjb25zdHJ1Y3RvcjogZnVuY3Rpb24oc3RhdGUsIGNvbnRleHQpIHtcbiAgICAgICAgICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKHRoaXMsIHtcbiAgICAgICAgICAgICAgc3RhdGU6IHtcbiAgICAgICAgICAgICAgICBlbnVtZXJhYmxlOiBmYWxzZSxcbiAgICAgICAgICAgICAgICB3cml0YWJsZTogdHJ1ZSxcbiAgICAgICAgICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgICAgICAgICAgdmFsdWU6IHN0YXRlXG4gICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgIGNvbnRleHQ6IHtcbiAgICAgICAgICAgICAgICBlbnVtZXJhYmxlOiBmYWxzZSxcbiAgICAgICAgICAgICAgICB3cml0YWJsZTogZmFsc2UsXG4gICAgICAgICAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlLFxuICAgICAgICAgICAgICAgIHZhbHVlOiBjb250ZXh0XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH0pO1xuICAgICAgICAgIH1cbiAgICAgICAgfSwgbWFrZUZpZWxkcyh0aGlzLnR5cGVzKSk7XG5cbiAgICAgICAgdGhpcy5jbGFzcyA9IG5ldyBDbGFzcyhwcm90byk7XG4gICAgICB9LFxuICAgICAgcmVhZDogZnVuY3Rpb24oaW5wdXQsIGNvbnRleHQpIHtcbiAgICAgICAgcmV0dXJuIG5ldyB0aGlzLmNsYXNzKGlucHV0LCBjb250ZXh0KTtcbiAgICAgIH0sXG4gICAgICB3cml0ZTogZnVuY3Rpb24oaW5wdXQsIGNvbnRleHQpIHtcbiAgICAgICAgdmFyIG91dHB1dCA9IHt9O1xuICAgICAgICBmb3IgKHZhciBrZXkgaW4gaW5wdXQpIHtcbiAgICAgICAgICBvdXRwdXRba2V5XSA9IHdyaXRlKHZhbHVlLCBjb250ZXh0LCB0eXBlc1trZXldKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gb3V0cHV0O1xuICAgICAgfVxuICAgIH0pO1xuXG4gICAgdmFyIG1ha2VNZXRob2RzID0gZnVuY3Rpb24oZGVzY3JpcHRvcnMpIHtcbiAgICAgIHJldHVybiBkZXNjcmlwdG9ycy5yZWR1Y2UoZnVuY3Rpb24obWV0aG9kcywgZGVzY3JpcHRvcikge1xuICAgICAgICBtZXRob2RzW2Rlc2NyaXB0b3IubmFtZV0gPSB7XG4gICAgICAgICAgZW51bWVyYWJsZTogdHJ1ZSxcbiAgICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgICAgd3JpdGFibGU6IGZhbHNlLFxuICAgICAgICAgIHZhbHVlOiBtYWtlTWV0aG9kKGRlc2NyaXB0b3IpXG4gICAgICAgIH07XG4gICAgICAgIHJldHVybiBtZXRob2RzO1xuICAgICAgfSwge30pO1xuICAgIH07XG5cbiAgICB2YXIgbWFrZUV2ZW50cyA9IGZ1bmN0aW9uKGRlc2NyaXB0b3JzKSB7XG4gICAgICByZXR1cm4gcGFpcnMoZGVzY3JpcHRvcnMpLnJlZHVjZShmdW5jdGlvbihldmVudHMsIHBhaXIpIHtcbiAgICAgICAgdmFyIG5hbWUgPSBwYWlyWzBdLCBkZXNjcmlwdG9yID0gcGFpclsxXTtcbiAgICAgICAgdmFyIGV2ZW50ID0gbmV3IEV2ZW50KG5hbWUsIGRlc2NyaXB0b3IpO1xuICAgICAgICBldmVudHNbZXZlbnQuZXZlbnRUeXBlXSA9IGV2ZW50O1xuICAgICAgICByZXR1cm4gZXZlbnRzO1xuICAgICAgfSwgT2JqZWN0LmNyZWF0ZShudWxsKSk7XG4gICAgfTtcblxuICAgIHZhciBBY3RvciA9IENsYXNzKHtcbiAgICAgIGV4dGVuZHM6IFR5cGUsXG4gICAgICBjYXRlZ29yeTogXCJhY3RvclwiLFxuICAgICAgZ2V0IG5hbWUoKSB7IHJldHVybiB0aGlzLnR5cGU7IH0sXG4gICAgICBjb25zdHJ1Y3RvcjogZnVuY3Rpb24oZGVzY3JpcHRvcikge1xuICAgICAgICB0aGlzLnR5cGUgPSBkZXNjcmlwdG9yLnR5cGVOYW1lO1xuXG4gICAgICAgIHZhciBldmVudHMgPSBtYWtlRXZlbnRzKGRlc2NyaXB0b3IuZXZlbnRzIHx8IHt9KTtcbiAgICAgICAgdmFyIGZpZWxkcyA9IG1ha2VGaWVsZHMoZGVzY3JpcHRvci5maWVsZHMgfHwge30pO1xuICAgICAgICB2YXIgbWV0aG9kcyA9IG1ha2VNZXRob2RzKGRlc2NyaXB0b3IubWV0aG9kcyB8fCBbXSk7XG5cblxuICAgICAgICB2YXIgcHJvdG8gPSB7XG4gICAgICAgICAgZXh0ZW5kczogRnJvbnQsXG4gICAgICAgICAgY29uc3RydWN0b3I6IGZ1bmN0aW9uKCkge1xuICAgICAgICAgICAgRnJvbnQuYXBwbHkodGhpcywgYXJndW1lbnRzKTtcbiAgICAgICAgICB9LFxuICAgICAgICAgIGV2ZW50czogZXZlbnRzXG4gICAgICAgIH07XG4gICAgICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKHByb3RvLCBmaWVsZHMpO1xuICAgICAgICBPYmplY3QuZGVmaW5lUHJvcGVydGllcyhwcm90bywgbWV0aG9kcyk7XG5cbiAgICAgICAgdGhpcy5jbGFzcyA9IENsYXNzKHByb3RvKTtcbiAgICAgIH0sXG4gICAgICByZWFkOiBmdW5jdGlvbihpbnB1dCwgY29udGV4dCwgZGV0YWlsKSB7XG4gICAgICAgIHZhciBzdGF0ZSA9IHR5cGVvZihpbnB1dCkgPT09IFwic3RyaW5nXCIgPyB7IGFjdG9yOiBpbnB1dCB9IDogaW5wdXQ7XG5cbiAgICAgICAgdmFyIGFjdG9yID0gY2xpZW50LmdldChzdGF0ZS5hY3RvcikgfHwgbmV3IHRoaXMuY2xhc3Moc3RhdGUsIGNvbnRleHQpO1xuICAgICAgICBhY3Rvci5mb3JtKHN0YXRlLCBkZXRhaWwsIGNvbnRleHQpO1xuXG4gICAgICAgIHJldHVybiBhY3RvcjtcbiAgICAgIH0sXG4gICAgICB3cml0ZTogZnVuY3Rpb24oaW5wdXQsIGNvbnRleHQsIGRldGFpbCkge1xuICAgICAgICByZXR1cm4gaW5wdXQuaWQ7XG4gICAgICB9XG4gICAgfSk7XG4gICAgZXhwb3J0cy5BY3RvciA9IEFjdG9yO1xuXG5cbiAgICB2YXIgQWN0b3JEZXRhaWwgPSBDbGFzcyh7XG4gICAgICBleHRlbmRzOiBBY3RvcixcbiAgICAgIGNvbnN0cnVjdG9yOiBmdW5jdGlvbihuYW1lKSB7XG4gICAgICAgIHZhciBwYXJ0cyA9IG5hbWUuc3BsaXQoXCIjXCIpXG4gICAgICAgIHRoaXMuYWN0b3JUeXBlID0gcGFydHNbMF1cbiAgICAgICAgdGhpcy5kZXRhaWwgPSBwYXJ0c1sxXTtcbiAgICAgIH0sXG4gICAgICByZWFkOiBmdW5jdGlvbihpbnB1dCwgY29udGV4dCkge1xuICAgICAgICByZXR1cm4gdHlwZUZvcih0aGlzLmFjdG9yVHlwZSkucmVhZChpbnB1dCwgY29udGV4dCwgdGhpcy5kZXRhaWwpO1xuICAgICAgfSxcbiAgICAgIHdyaXRlOiBmdW5jdGlvbihpbnB1dCwgY29udGV4dCkge1xuICAgICAgICByZXR1cm4gdHlwZUZvcih0aGlzLmFjdG9yVHlwZSkud3JpdGUoaW5wdXQsIGNvbnRleHQsIHRoaXMuZGV0YWlsKTtcbiAgICAgIH1cbiAgICB9KTtcbiAgICBleHBvcnRzLkFjdG9yRGV0YWlsID0gQWN0b3JEZXRhaWw7XG5cbiAgICB2YXIgTWV0aG9kID0gQ2xhc3Moe1xuICAgICAgZXh0ZW5kczogVHlwZSxcbiAgICAgIGNvbnN0cnVjdG9yOiBmdW5jdGlvbihkZXNjcmlwdG9yKSB7XG4gICAgICAgIHRoaXMudHlwZSA9IGRlc2NyaXB0b3IubmFtZTtcbiAgICAgICAgdGhpcy5wYXRoID0gZmluZFBhdGgoZGVzY3JpcHRvci5yZXNwb25zZSwgXCJfcmV0dmFsXCIpO1xuICAgICAgICB0aGlzLnJlc3BvbnNlVHlwZSA9IHRoaXMucGF0aCAmJiBxdWVyeShkZXNjcmlwdG9yLnJlc3BvbnNlLCB0aGlzLnBhdGgpLl9yZXR2YWw7XG4gICAgICAgIHRoaXMucmVxdWVzdFR5cGUgPSBkZXNjcmlwdG9yLnJlcXVlc3QudHlwZTtcblxuICAgICAgICB2YXIgcGFyYW1zID0gW107XG4gICAgICAgIGZvciAodmFyIGtleSBpbiBkZXNjcmlwdG9yLnJlcXVlc3QpIHtcbiAgICAgICAgICBpZiAoa2V5ICE9PSBcInR5cGVcIikge1xuICAgICAgICAgICAgdmFyIHBhcmFtID0gZGVzY3JpcHRvci5yZXF1ZXN0W2tleV07XG4gICAgICAgICAgICB2YXIgaW5kZXggPSBcIl9hcmdcIiBpbiBwYXJhbSA/IHBhcmFtLl9hcmcgOiBwYXJhbS5fb3B0aW9uO1xuICAgICAgICAgICAgdmFyIGlzUGFyYW0gPSBwYXJhbS5fb3B0aW9uID09PSBpbmRleDtcbiAgICAgICAgICAgIHZhciBpc0FyZ3VtZW50ID0gcGFyYW0uX2FyZyA9PT0gaW5kZXg7XG4gICAgICAgICAgICBwYXJhbXNbaW5kZXhdID0ge1xuICAgICAgICAgICAgICB0eXBlOiBwYXJhbS50eXBlLFxuICAgICAgICAgICAgICBrZXk6IGtleSxcbiAgICAgICAgICAgICAgaW5kZXg6IGluZGV4LFxuICAgICAgICAgICAgICBpc1BhcmFtOiBpc1BhcmFtLFxuICAgICAgICAgICAgICBpc0FyZ3VtZW50OiBpc0FyZ3VtZW50XG4gICAgICAgICAgICB9O1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgICB0aGlzLnBhcmFtcyA9IHBhcmFtcztcbiAgICAgIH0sXG4gICAgICByZWFkOiBmdW5jdGlvbihpbnB1dCwgY29udGV4dCkge1xuICAgICAgICByZXR1cm4gcmVhZChxdWVyeShpbnB1dCwgdGhpcy5wYXRoKSwgY29udGV4dCwgdGhpcy5yZXNwb25zZVR5cGUpO1xuICAgICAgfSxcbiAgICAgIHdyaXRlOiBmdW5jdGlvbihpbnB1dCwgY29udGV4dCkge1xuICAgICAgICByZXR1cm4gdGhpcy5wYXJhbXMucmVkdWNlKGZ1bmN0aW9uKHJlc3VsdCwgcGFyYW0pIHtcbiAgICAgICAgICByZXN1bHRbcGFyYW0ua2V5XSA9IHdyaXRlKGlucHV0W3BhcmFtLmluZGV4XSwgY29udGV4dCwgcGFyYW0udHlwZSk7XG4gICAgICAgICAgcmV0dXJuIHJlc3VsdDtcbiAgICAgICAgfSwge3R5cGU6IHRoaXMudHlwZX0pO1xuICAgICAgfVxuICAgIH0pO1xuICAgIGV4cG9ydHMuTWV0aG9kID0gTWV0aG9kO1xuXG4gICAgdmFyIHByb2ZpbGVyID0gZnVuY3Rpb24obWV0aG9kLCBpZCkge1xuICAgICAgcmV0dXJuIGZ1bmN0aW9uKCkge1xuICAgICAgICB2YXIgc3RhcnQgPSBuZXcgRGF0ZSgpO1xuICAgICAgICByZXR1cm4gbWV0aG9kLmFwcGx5KHRoaXMsIGFyZ3VtZW50cykudGhlbihmdW5jdGlvbihyZXN1bHQpIHtcbiAgICAgICAgICB2YXIgZW5kID0gbmV3IERhdGUoKTtcbiAgICAgICAgICBjbGllbnQudGVsZW1ldHJ5LmFkZChpZCwgK2VuZCAtIHN0YXJ0KTtcbiAgICAgICAgICByZXR1cm4gcmVzdWx0O1xuICAgICAgICB9KTtcbiAgICAgIH07XG4gICAgfTtcblxuICAgIHZhciBkZXN0cnVjdG9yID0gZnVuY3Rpb24obWV0aG9kKSB7XG4gICAgICByZXR1cm4gZnVuY3Rpb24oKSB7XG4gICAgICAgIHJldHVybiBtZXRob2QuYXBwbHkodGhpcywgYXJndW1lbnRzKS50aGVuKGZ1bmN0aW9uKHJlc3VsdCkge1xuICAgICAgICAgIGNsaWVudC5yZWxlYXNlKHRoaXMpO1xuICAgICAgICAgIHJldHVybiByZXN1bHQ7XG4gICAgICAgIH0pO1xuICAgICAgfTtcbiAgICB9O1xuXG4gICAgZnVuY3Rpb24gbWFrZU1ldGhvZChkZXNjcmlwdG9yKSB7XG4gICAgICB2YXIgdHlwZSA9IG5ldyBNZXRob2QoZGVzY3JpcHRvcik7XG4gICAgICB2YXIgbWV0aG9kID0gZGVzY3JpcHRvci5vbmV3YXkgPyBtYWtlVW5pZGlyZWNhdGlvbmFsTWV0aG9kKGRlc2NyaXB0b3IsIHR5cGUpIDpcbiAgICAgICAgICAgICAgICAgICBtYWtlQmlkaXJlY3Rpb25hbE1ldGhvZChkZXNjcmlwdG9yLCB0eXBlKTtcblxuICAgICAgaWYgKGRlc2NyaXB0b3IudGVsZW1ldHJ5KVxuICAgICAgICBtZXRob2QgPSBwcm9maWxlcihtZXRob2QpO1xuICAgICAgaWYgKGRlc2NyaXB0b3IucmVsZWFzZSlcbiAgICAgICAgbWV0aG9kID0gZGVzdHJ1Y3RvcihtZXRob2QpO1xuXG4gICAgICByZXR1cm4gbWV0aG9kO1xuICAgIH1cblxuICAgIHZhciBtYWtlVW5pZGlyZWNhdGlvbmFsTWV0aG9kID0gZnVuY3Rpb24oZGVzY3JpcHRvciwgdHlwZSkge1xuICAgICAgcmV0dXJuIGZ1bmN0aW9uKCkge1xuICAgICAgICB2YXIgcGFja2V0ID0gdHlwZS53cml0ZShhcmd1bWVudHMsIHRoaXMpO1xuICAgICAgICBwYWNrZXQudG8gPSB0aGlzLmlkO1xuICAgICAgICBjbGllbnQuc2VuZChwYWNrZXQpO1xuICAgICAgICByZXR1cm4gUHJvbWlzZS5yZXNvbHZlKHZvaWQoMCkpO1xuICAgICAgfTtcbiAgICB9O1xuXG4gICAgdmFyIG1ha2VCaWRpcmVjdGlvbmFsTWV0aG9kID0gZnVuY3Rpb24oZGVzY3JpcHRvciwgdHlwZSkge1xuICAgICAgcmV0dXJuIGZ1bmN0aW9uKCkge1xuICAgICAgICB2YXIgY29udGV4dCA9IHRoaXMuY29udGV4dDtcbiAgICAgICAgdmFyIHBhY2tldCA9IHR5cGUud3JpdGUoYXJndW1lbnRzLCBjb250ZXh0KTtcbiAgICAgICAgdmFyIGNvbnRleHQgPSB0aGlzLmNvbnRleHQ7XG4gICAgICAgIHBhY2tldC50byA9IHRoaXMuaWQ7XG4gICAgICAgIHJldHVybiBjbGllbnQucmVxdWVzdChwYWNrZXQpLnRoZW4oZnVuY3Rpb24ocGFja2V0KSB7XG4gICAgICAgICAgcmV0dXJuIHR5cGUucmVhZChwYWNrZXQsIGNvbnRleHQpO1xuICAgICAgICB9KTtcbiAgICAgIH07XG4gICAgfTtcblxuICAgIHZhciBFdmVudCA9IENsYXNzKHtcbiAgICAgIGNvbnN0cnVjdG9yOiBmdW5jdGlvbihuYW1lLCBkZXNjcmlwdG9yKSB7XG4gICAgICAgIHRoaXMubmFtZSA9IGRlc2NyaXB0b3IudHlwZSB8fCBuYW1lO1xuICAgICAgICB0aGlzLmV2ZW50VHlwZSA9IGRlc2NyaXB0b3IudHlwZSB8fCBuYW1lO1xuICAgICAgICB0aGlzLnR5cGVzID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcblxuICAgICAgICB2YXIgdHlwZXMgPSB0aGlzLnR5cGVzO1xuICAgICAgICBmb3IgKHZhciBrZXkgaW4gZGVzY3JpcHRvcikge1xuICAgICAgICAgIGlmIChrZXkgPT09IFwidHlwZVwiKSB7XG4gICAgICAgICAgICB0eXBlc1trZXldID0gXCJzdHJpbmdcIjtcbiAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgdHlwZXNba2V5XSA9IGRlc2NyaXB0b3Jba2V5XS50eXBlO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfSxcbiAgICAgIHJlYWQ6IGZ1bmN0aW9uKGlucHV0LCBjb250ZXh0KSB7XG4gICAgICAgIHZhciBvdXRwdXQgPSB7fTtcbiAgICAgICAgdmFyIHR5cGVzID0gdGhpcy50eXBlcztcbiAgICAgICAgZm9yICh2YXIga2V5IGluIGlucHV0KSB7XG4gICAgICAgICAgb3V0cHV0W2tleV0gPSByZWFkKGlucHV0W2tleV0sIGNvbnRleHQsIHR5cGVzW2tleV0pO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBvdXRwdXQ7XG4gICAgICB9LFxuICAgICAgd3JpdGU6IGZ1bmN0aW9uKGlucHV0LCBjb250ZXh0KSB7XG4gICAgICAgIHZhciBvdXRwdXQgPSB7fTtcbiAgICAgICAgdmFyIHR5cGVzID0gdGhpcy50eXBlcztcbiAgICAgICAgZm9yICh2YXIga2V5IGluIHRoaXMudHlwZXMpIHtcbiAgICAgICAgICBvdXRwdXRba2V5XSA9IHdyaXRlKGlucHV0W2tleV0sIGNvbnRleHQsIHR5cGVzW2tleV0pO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBvdXRwdXQ7XG4gICAgICB9XG4gICAgfSk7XG5cbiAgICB2YXIgRnJvbnQgPSBDbGFzcyh7XG4gICAgICBleHRlbmRzOiBFdmVudFRhcmdldCxcbiAgICAgIEV2ZW50VGFyZ2V0OiBFdmVudFRhcmdldCxcbiAgICAgIGNvbnN0cnVjdG9yOiBmdW5jdGlvbihzdGF0ZSkge1xuICAgICAgICB0aGlzLkV2ZW50VGFyZ2V0KCk7XG4gICAgICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKHRoaXMsICB7XG4gICAgICAgICAgc3RhdGU6IHtcbiAgICAgICAgICAgIGVudW1lcmFibGU6IGZhbHNlLFxuICAgICAgICAgICAgd3JpdGFibGU6IHRydWUsXG4gICAgICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgICAgICB2YWx1ZTogc3RhdGVcbiAgICAgICAgICB9XG4gICAgICAgIH0pO1xuXG4gICAgICAgIGNsaWVudC5yZWdpc3Rlcih0aGlzKTtcbiAgICAgIH0sXG4gICAgICBnZXQgaWQoKSB7XG4gICAgICAgIHJldHVybiB0aGlzLnN0YXRlLmFjdG9yO1xuICAgICAgfSxcbiAgICAgIGdldCBjb250ZXh0KCkge1xuICAgICAgICByZXR1cm4gdGhpcztcbiAgICAgIH0sXG4gICAgICBmb3JtOiBmdW5jdGlvbihzdGF0ZSwgZGV0YWlsLCBjb250ZXh0KSB7XG4gICAgICAgIGlmICh0aGlzLnN0YXRlICE9PSBzdGF0ZSkge1xuICAgICAgICAgIGlmIChkZXRhaWwpIHtcbiAgICAgICAgICAgIHRoaXMuc3RhdGVbZGV0YWlsXSA9IHN0YXRlW2RldGFpbF07XG4gICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIHBhaXJzKHN0YXRlKS5mb3JFYWNoKGZ1bmN0aW9uKHBhaXIpIHtcbiAgICAgICAgICAgICAgdmFyIGtleSA9IHBhaXJbMF0sIHZhbHVlID0gcGFpclsxXTtcbiAgICAgICAgICAgICAgdGhpcy5zdGF0ZVtrZXldID0gdmFsdWU7XG4gICAgICAgICAgICB9LCB0aGlzKTtcbiAgICAgICAgICB9XG4gICAgICAgIH1cblxuICAgICAgICBpZiAoY29udGV4dCkge1xuICAgICAgICAgIGNsaWVudC5zdXBlcnZpc2UoY29udGV4dCwgdGhpcyk7XG4gICAgICAgIH1cbiAgICAgIH0sXG4gICAgICByZXF1ZXN0VHlwZXM6IGZ1bmN0aW9uKCkge1xuICAgICAgICByZXR1cm4gY2xpZW50LnJlcXVlc3Qoe1xuICAgICAgICAgIHRvOiB0aGlzLmlkLFxuICAgICAgICAgIHR5cGU6IFwicmVxdWVzdFR5cGVzXCJcbiAgICAgICAgfSkudGhlbihmdW5jdGlvbihwYWNrZXQpIHtcbiAgICAgICAgICByZXR1cm4gcGFja2V0LnJlcXVlc3RUeXBlcztcbiAgICAgICAgfSk7XG4gICAgICB9XG4gICAgfSk7XG4gICAgdHlwZXMucHJpbWl0aXZlID0gbmV3IFByaW1pdHZlKFwicHJpbWl0aXZlXCIpO1xuICAgIHR5cGVzLnN0cmluZyA9IG5ldyBQcmltaXR2ZShcInN0cmluZ1wiKTtcbiAgICB0eXBlcy5udW1iZXIgPSBuZXcgUHJpbWl0dmUoXCJudW1iZXJcIik7XG4gICAgdHlwZXMuYm9vbGVhbiA9IG5ldyBQcmltaXR2ZShcImJvb2xlYW5cIik7XG4gICAgdHlwZXMuanNvbiA9IG5ldyBQcmltaXR2ZShcImpzb25cIik7XG4gICAgdHlwZXMuYXJyYXkgPSBuZXcgUHJpbWl0dmUoXCJhcnJheVwiKTtcbiAgfSxcbiAgcmVnaXN0ZXJUeXBlczogZnVuY3Rpb24oZGVzY3JpcHRvcikge1xuICAgIHZhciBzcGVjaWZpY2F0aW9uID0gdGhpcy5zcGVjaWZpY2F0aW9uO1xuICAgIHZhbHVlcyhkZXNjcmlwdG9yLnR5cGVzKS5mb3JFYWNoKGZ1bmN0aW9uKGRlc2NyaXB0b3IpIHtcbiAgICAgIHNwZWNpZmljYXRpb25bZGVzY3JpcHRvci50eXBlTmFtZV0gPSBkZXNjcmlwdG9yO1xuICAgIH0pO1xuICB9XG59KTtcbmV4cG9ydHMuVHlwZVN5c3RlbSA9IFR5cGVTeXN0ZW07XG4iLCJcInVzZSBzdHJpY3RcIjtcblxudmFyIGtleXMgPSBPYmplY3Qua2V5cztcbmV4cG9ydHMua2V5cyA9IGtleXM7XG5cbi8vIFJldHVybnMgYXJyYXkgb2YgdmFsdWVzIGZvciB0aGUgZ2l2ZW4gb2JqZWN0LlxudmFyIHZhbHVlcyA9IGZ1bmN0aW9uKG9iamVjdCkge1xuICByZXR1cm4ga2V5cyhvYmplY3QpLm1hcChmdW5jdGlvbihrZXkpIHtcbiAgICByZXR1cm4gb2JqZWN0W2tleV1cbiAgfSk7XG59O1xuZXhwb3J0cy52YWx1ZXMgPSB2YWx1ZXM7XG5cbi8vIFJldHVybnMgW2tleSwgdmFsdWVdIHBhaXJzIGZvciB0aGUgZ2l2ZW4gb2JqZWN0LlxudmFyIHBhaXJzID0gZnVuY3Rpb24ob2JqZWN0KSB7XG4gIHJldHVybiBrZXlzKG9iamVjdCkubWFwKGZ1bmN0aW9uKGtleSkge1xuICAgIHJldHVybiBba2V5LCBvYmplY3Rba2V5XV1cbiAgfSk7XG59O1xuZXhwb3J0cy5wYWlycyA9IHBhaXJzO1xuXG5cbi8vIFF1ZXJpZXMgYW4gb2JqZWN0IGZvciB0aGUgZmllbGQgbmVzdGVkIHdpdGggaW4gaXQuXG52YXIgcXVlcnkgPSBmdW5jdGlvbihvYmplY3QsIHBhdGgpIHtcbiAgcmV0dXJuIHBhdGgucmVkdWNlKGZ1bmN0aW9uKG9iamVjdCwgZW50cnkpIHtcbiAgICByZXR1cm4gb2JqZWN0ICYmIG9iamVjdFtlbnRyeV1cbiAgfSwgb2JqZWN0KTtcbn07XG5leHBvcnRzLnF1ZXJ5ID0gcXVlcnk7XG5cbnZhciBpc09iamVjdCA9IGZ1bmN0aW9uKHgpIHtcbiAgcmV0dXJuIHggJiYgdHlwZW9mKHgpID09PSBcIm9iamVjdFwiXG59XG5cbnZhciBmaW5kUGF0aCA9IGZ1bmN0aW9uKG9iamVjdCwga2V5KSB7XG4gIHZhciBwYXRoID0gdm9pZCgwKTtcbiAgaWYgKG9iamVjdCAmJiB0eXBlb2Yob2JqZWN0KSA9PT0gXCJvYmplY3RcIikge1xuICAgIHZhciBuYW1lcyA9IGtleXMob2JqZWN0KTtcbiAgICBpZiAobmFtZXMuaW5kZXhPZihrZXkpID49IDApIHtcbiAgICAgIHBhdGggPSBbXTtcbiAgICB9IGVsc2Uge1xuICAgICAgdmFyIGluZGV4ID0gMDtcbiAgICAgIHZhciBjb3VudCA9IG5hbWVzLmxlbmd0aDtcbiAgICAgIHdoaWxlIChpbmRleCA8IGNvdW50ICYmICFwYXRoKXtcbiAgICAgICAgdmFyIGhlYWQgPSBuYW1lc1tpbmRleF07XG4gICAgICAgIHZhciB0YWlsID0gZmluZFBhdGgob2JqZWN0W2hlYWRdLCBrZXkpO1xuICAgICAgICBwYXRoID0gdGFpbCA/IFtoZWFkXS5jb25jYXQodGFpbCkgOiB0YWlsO1xuICAgICAgICBpbmRleCA9IGluZGV4ICsgMVxuICAgICAgfVxuICAgIH1cbiAgfVxuICByZXR1cm4gcGF0aDtcbn07XG5leHBvcnRzLmZpbmRQYXRoID0gZmluZFBhdGg7XG4iXX0= +(1) +}); diff --git a/addon-sdk/source/lib/diffpatcher/.travis.yml b/addon-sdk/source/lib/diffpatcher/.travis.yml new file mode 100644 index 000000000..780731a47 --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - 0.4 + - 0.5 + - 0.6 diff --git a/addon-sdk/source/lib/diffpatcher/History.md b/addon-sdk/source/lib/diffpatcher/History.md new file mode 100644 index 000000000..d38978805 --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/History.md @@ -0,0 +1,14 @@ +# Changes + +## 1.0.1 / 2013-05-01 + + - Update method library version. + +## 1.0.0 / 2012-11-09 + + - Test integration for browsers. + - New method library. + +## 0.0.1 / 2012-10-22 + + - Initial release diff --git a/addon-sdk/source/lib/diffpatcher/License.md b/addon-sdk/source/lib/diffpatcher/License.md new file mode 100644 index 000000000..ed76489a3 --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/License.md @@ -0,0 +1,18 @@ +Copyright 2012 Irakli Gozalishvili. All rights reserved. +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. diff --git a/addon-sdk/source/lib/diffpatcher/Readme.md b/addon-sdk/source/lib/diffpatcher/Readme.md new file mode 100644 index 000000000..1520b1c37 --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/Readme.md @@ -0,0 +1,70 @@ +# diffpatcher + +[![Build Status](https://secure.travis-ci.org/Gozala/diffpatcher.png)](http://travis-ci.org/Gozala/diffpatcher) + +[![Browser support](https://ci.testling.com/Gozala/diffpatcher.png)](http://ci.testling.com/Gozala/diffpatcher) + +Diffpatcher is a small library that lets you treat hashes as if they were +git repositories. + +## diff + +Diff function that takes two hashes and returns delta hash. + +```js +var diff = require("diffpatcher/diff") + +diff({ a: { b: 1 }, c: { d: 2 } }, // hash#1 + { a: { e: 3 }, c: { d: 4 } }) // hash#2 + +// => { // delta +// a: { +// b: null, // - +// e: 3 // + +// }, +// c: { +// d: 4 // ± +// } +// } +``` + +As you can see from the example above `delta` makes no real distinction between +proprety upadate and property addition. Try to think of additions as an update +from `undefined` to whatever it's being updated to. + +## patch + +Patch fuction takes a `hash` and a `delta` and returns a new `hash` which is +just like orginial but with delta applied to it. Let's apply delta from the +previous example to the first hash from the same example + + +```js +var patch = require("diffpatcher/patch") + +patch({ a: { b: 1 }, c: { d: 2 } }, // hash#1 + { // delta + a: { + b: null, // - + e: 3 // + + }, + c: { + d: 4 // ± + } + }) + +// => { a: { e: 3 }, c: { d: 4 } } // hash#2 +``` + +That's about it really, just diffing hashes and applying thes diffs on them. + + +### rebase + +And as Linus mentioned everything in git can be expressed with `rebase`, that +also pretty much the case for `diffpatcher`. `rebase` takes `target` hash, +and rebases `parent` onto it with `diff` applied. + +## Install + + npm install diffpatcher diff --git a/addon-sdk/source/lib/diffpatcher/diff.js b/addon-sdk/source/lib/diffpatcher/diff.js new file mode 100644 index 000000000..967c137e6 --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/diff.js @@ -0,0 +1,45 @@ +"use strict"; + +var method = require("../method/core") + +// Method is designed to work with data structures representing application +// state. Calling it with a state should return object representing `delta` +// that has being applied to a previous state to get to a current state. +// +// Example +// +// diff(state) // => { "item-id-1": { title: "some title" } "item-id-2": null } +var diff = method("diff@diffpatcher") + +// diff between `null` / `undefined` to any hash is a hash itself. +diff.define(null, function(from, to) { return to }) +diff.define(undefined, function(from, to) { return to }) +diff.define(Object, function(from, to) { + return calculate(from, to || {}) || {} +}) + +function calculate(from, to) { + var diff = {} + var changes = 0 + Object.keys(from).forEach(function(key) { + changes = changes + 1 + if (!(key in to) && from[key] != null) diff[key] = null + else changes = changes - 1 + }) + Object.keys(to).forEach(function(key) { + changes = changes + 1 + var previous = from[key] + var current = to[key] + if (previous === current) return (changes = changes - 1) + if (typeof(current) !== "object") return diff[key] = current + if (typeof(previous) !== "object") return diff[key] = current + var delta = calculate(previous, current) + if (delta) diff[key] = delta + else changes = changes - 1 + }) + return changes ? diff : null +} + +diff.calculate = calculate + +module.exports = diff diff --git a/addon-sdk/source/lib/diffpatcher/index.js b/addon-sdk/source/lib/diffpatcher/index.js new file mode 100644 index 000000000..91ddba425 --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/index.js @@ -0,0 +1,5 @@ +"use strict"; + +exports.diff = require("./diff") +exports.patch = require("./patch") +exports.rebase = require("./rebase") diff --git a/addon-sdk/source/lib/diffpatcher/package.json b/addon-sdk/source/lib/diffpatcher/package.json new file mode 100644 index 000000000..54e085d2e --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/package.json @@ -0,0 +1,54 @@ +{ + "name": "diffpatcher", + "id": "diffpatcher", + "version": "1.2.0", + "description": "Utilities for diff-ing & patch-ing hashes", + "keywords": [ + "diff", "patch", "rebase", "hash", "changes", "versions" + ], + "author": "Irakli Gozalishvili (http://jeditoolkit.com)", + "homepage": "https://github.com/Gozala/diffpatcher", + "repository": { + "type": "git", + "url": "https://github.com/Gozala/diffpatcher.git", + "web": "https://github.com/Gozala/diffpatcher" + }, + "bugs": { + "url": "http://github.com/Gozala/diffpatcher/issues/" + }, + "dependencies": { + "method": "~2.0.0" + }, + "devDependencies": { + "test": "~0.x.0", + "phantomify": "~0.x.0", + "retape": "~0.x.0", + "tape": "~0.1.5" + }, + "main": "./index.js", + "scripts": { + "test": "npm run test-node && npm run test-browser", + "test-browser": "node ./node_modules/phantomify/bin/cmd.js ./test/common.js", + "test-node": "node ./test/common.js", + "test-tap": "node ./test/tap.js" + }, + "testling": { + "files": "test/tap.js", + "browsers": [ + "ie/9..latest", + "chrome/25..latest", + "firefox/20..latest", + "safari/6..latest", + "opera/11.0..latest", + "iphone/6..latest", + "ipad/6..latest", + "android-browser/4.2..latest" + ] + }, + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/Gozala/diffpatcher/License.md" + } + ] +} diff --git a/addon-sdk/source/lib/diffpatcher/patch.js b/addon-sdk/source/lib/diffpatcher/patch.js new file mode 100644 index 000000000..9271e8893 --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/patch.js @@ -0,0 +1,21 @@ +"use strict"; + +var method = require("../method/core") +var rebase = require("./rebase") + +// Method is designed to work with data structures representing application +// state. Calling it with a state and delta should return object representing +// new state, with changes in `delta` being applied to previous. +// +// ## Example +// +// patch(state, { +// "item-id-1": { completed: false }, // update +// "item-id-2": null // delete +// }) +var patch = method("patch@diffpatcher") +patch.define(Object, function patch(hash, delta) { + return rebase({}, hash, delta) +}) + +module.exports = patch diff --git a/addon-sdk/source/lib/diffpatcher/rebase.js b/addon-sdk/source/lib/diffpatcher/rebase.js new file mode 100644 index 000000000..03c756fee --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/rebase.js @@ -0,0 +1,36 @@ +"use strict"; + +var nil = {} +var owns = ({}).hasOwnProperty + +function rebase(result, parent, delta) { + var key, current, previous, update + for (key in parent) { + if (owns.call(parent, key)) { + previous = parent[key] + update = owns.call(delta, key) ? delta[key] : nil + if (previous === null) continue + else if (previous === void(0)) continue + else if (update === null) continue + else if (update === void(0)) continue + else result[key] = previous + } + } + for (key in delta) { + if (owns.call(delta, key)) { + update = delta[key] + current = owns.call(result, key) ? result[key] : nil + if (current === update) continue + else if (update === null) continue + else if (update === void(0)) continue + else if (current === nil) result[key] = update + else if (typeof(update) !== "object") result[key] = update + else if (typeof(current) !== "object") result[key] = update + else result[key]= rebase({}, current, update) + } + } + + return result +} + +module.exports = rebase diff --git a/addon-sdk/source/lib/diffpatcher/test/common.js b/addon-sdk/source/lib/diffpatcher/test/common.js new file mode 100644 index 000000000..dbc79013c --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/test/common.js @@ -0,0 +1,3 @@ +"use strict"; + +require("test").run(require("./index")) diff --git a/addon-sdk/source/lib/diffpatcher/test/diff.js b/addon-sdk/source/lib/diffpatcher/test/diff.js new file mode 100644 index 000000000..d1d674005 --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/test/diff.js @@ -0,0 +1,59 @@ +"use strict"; + +var diff = require("../diff") + +exports["test diff from null"] = function(assert) { + var to = { a: 1, b: 2 } + assert.equal(diff(null, to), to, "diff null to x returns x") + assert.equal(diff(void(0), to), to, "diff undefined to x returns x") + +} + +exports["test diff to null"] = function(assert) { + var from = { a: 1, b: 2 } + assert.deepEqual(diff({ a: 1, b: 2 }, null), + { a: null, b: null }, + "diff x null returns x with all properties nullified") +} + +exports["test diff identical"] = function(assert) { + assert.deepEqual(diff({}, {}), {}, "diff on empty objects is {}") + + assert.deepEqual(diff({ a: 1, b: 2 }, { a: 1, b: 2 }), {}, + "if properties match diff is {}") + + assert.deepEqual(diff({ a: 1, b: { c: { d: 3, e: 4 } } }, + { a: 1, b: { c: { d: 3, e: 4 } } }), {}, + "diff between identical nested hashes is {}") + +} + +exports["test diff delete"] = function(assert) { + assert.deepEqual(diff({ a: 1, b: 2 }, { b: 2 }), { a: null }, + "missing property is deleted") + assert.deepEqual(diff({ a: 1, b: 2 }, { a: 2 }), { a: 2, b: null }, + "missing property is deleted another updated") + assert.deepEqual(diff({ a: 1, b: 2 }, {}), { a: null, b: null }, + "missing propertes are deleted") + assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, {}), + { a: null, b: null }, + "missing deep propertes are deleted") + assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, { b: { c: {} } }), + { a: null, b: { c: { d: null } } }, + "missing nested propertes are deleted") +} + +exports["test add update"] = function(assert) { + assert.deepEqual(diff({ a: 1, b: 2 }, { b: 2, c: 3 }), { a: null, c: 3 }, + "delete and add") + assert.deepEqual(diff({ a: 1, b: 2 }, { a: 2, c: 3 }), { a: 2, b: null, c: 3 }, + "delete and adds") + assert.deepEqual(diff({}, { a: 1, b: 2 }), { a: 1, b: 2 }, + "diff on empty objcet returns equivalen of to") + assert.deepEqual(diff({ a: 1, b: { c: { d: 2 } } }, { d: 3 }), + { a: null, b: null, d: 3 }, + "missing deep propertes are deleted") + assert.deepEqual(diff({ b: { c: {} }, d: null }, { a: 1, b: { c: { d: 2 } } }), + { a: 1, b: { c: { d: 2 } } }, + "missing nested propertes are deleted") +} diff --git a/addon-sdk/source/lib/diffpatcher/test/index.js b/addon-sdk/source/lib/diffpatcher/test/index.js new file mode 100644 index 000000000..c06407e7c --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/test/index.js @@ -0,0 +1,14 @@ +"use strict"; + +var diff = require("../diff") +var patch = require("../patch") + +exports["test diff"] = require("./diff") +exports["test patch"] = require("./patch") + +exports["test patch(a, diff(a, b)) => b"] = function(assert) { + var a = { a: { b: 1 }, c: { d: 2 } } + var b = { a: { e: 3 }, c: { d: 4 } } + + assert.deepEqual(patch(a, diff(a, b)), b, "patch(a, diff(a, b)) => b") +} diff --git a/addon-sdk/source/lib/diffpatcher/test/patch.js b/addon-sdk/source/lib/diffpatcher/test/patch.js new file mode 100644 index 000000000..dc2e38229 --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/test/patch.js @@ -0,0 +1,83 @@ +"use strict"; + +var patch = require("../patch") + +exports["test patch delete"] = function(assert) { + var hash = { a: 1, b: 2 } + + assert.deepEqual(patch(hash, { a: null }), { b: 2 }, "null removes property") +} + +exports["test patch delete with void"] = function(assert) { + var hash = { a: 1, b: 2 } + + assert.deepEqual(patch(hash, { a: void(0) }), { b: 2 }, + "void(0) removes property") +} + +exports["test patch delete missing"] = function(assert) { + assert.deepEqual(patch({ a: 1, b: 2 }, { c: null }), + { a: 1, b: 2 }, + "null removes property if exists"); + + assert.deepEqual(patch({ a: 1, b: 2 }, { c: void(0) }), + { a: 1, b: 2 }, + "void removes property if exists"); +} + +exports["test delete deleted"] = function(assert) { + assert.deepEqual(patch({ a: null, b: 2, c: 3, d: void(0)}, + { a: void(0), b: null, d: null }), + {c: 3}, + "removed all existing and non existing"); +} + +exports["test update deleted"] = function(assert) { + assert.deepEqual(patch({ a: null, b: void(0), c: 3}, + { a: { b: 2 } }), + { a: { b: 2 }, c: 3 }, + "replace deleted"); +} + +exports["test patch delete with void"] = function(assert) { + var hash = { a: 1, b: 2 } + + assert.deepEqual(patch(hash, { a: void(0) }), { b: 2 }, + "void(0) removes property") +} + + +exports["test patch addition"] = function(assert) { + var hash = { a: 1, b: 2 } + + assert.deepEqual(patch(hash, { c: 3 }), { a: 1, b: 2, c: 3 }, + "new properties are added") +} + +exports["test patch addition"] = function(assert) { + var hash = { a: 1, b: 2 } + + assert.deepEqual(patch(hash, { c: 3 }), { a: 1, b: 2, c: 3 }, + "new properties are added") +} + +exports["test hash on itself"] = function(assert) { + var hash = { a: 1, b: 2 } + + assert.deepEqual(patch(hash, hash), hash, + "applying hash to itself returns hash itself") +} + +exports["test patch with empty delta"] = function(assert) { + var hash = { a: 1, b: 2 } + + assert.deepEqual(patch(hash, {}), hash, + "applying empty delta results in no changes") +} + +exports["test patch nested data"] = function(assert) { + assert.deepEqual(patch({ a: { b: 1 }, c: { d: 2 } }, + { a: { b: null, e: 3 }, c: { d: 4 } }), + { a: { e: 3 }, c: { d: 4 } }, + "nested structures can also be patched") +} diff --git a/addon-sdk/source/lib/diffpatcher/test/tap.js b/addon-sdk/source/lib/diffpatcher/test/tap.js new file mode 100644 index 000000000..e550b82f5 --- /dev/null +++ b/addon-sdk/source/lib/diffpatcher/test/tap.js @@ -0,0 +1,3 @@ +"use strict"; + +require("retape")(require("./index")) diff --git a/addon-sdk/source/lib/framescript/FrameScriptManager.jsm b/addon-sdk/source/lib/framescript/FrameScriptManager.jsm new file mode 100644 index 000000000..1ce6ceb07 --- /dev/null +++ b/addon-sdk/source/lib/framescript/FrameScriptManager.jsm @@ -0,0 +1,27 @@ +/* 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 globalMM = Components.classes["@mozilla.org/globalmessagemanager;1"]. + getService(Components.interfaces.nsIMessageListenerManager); + +// Load frame scripts from the same dir as this module. +// Since this JSM will be loaded using require(), PATH will be +// overridden while running tests, just like any other module. +const PATH = __URI__.replace('framescript/FrameScriptManager.jsm', ''); + +// Builds a unique loader ID for this runtime. We prefix with the SDK path so +// overriden versions of the SDK don't conflict +var LOADER_ID = 0; +this.getNewLoaderID = () => { + return PATH + ":" + LOADER_ID++; +} + +const frame_script = function(contentFrame, PATH) { + let { registerContentFrame } = Components.utils.import(PATH + 'framescript/content.jsm', {}); + registerContentFrame(contentFrame); +} +globalMM.loadFrameScript("data:,(" + frame_script.toString() + ")(this, " + JSON.stringify(PATH) + ");", true); + +this.EXPORTED_SYMBOLS = ['getNewLoaderID']; diff --git a/addon-sdk/source/lib/framescript/content.jsm b/addon-sdk/source/lib/framescript/content.jsm new file mode 100644 index 000000000..eaee26be3 --- /dev/null +++ b/addon-sdk/source/lib/framescript/content.jsm @@ -0,0 +1,94 @@ +/* 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: Cu, classes: Cc, interfaces: Ci } = Components; +const { Services } = Cu.import('resource://gre/modules/Services.jsm'); + +const cpmm = Cc['@mozilla.org/childprocessmessagemanager;1']. + getService(Ci.nsISyncMessageSender); + +this.EXPORTED_SYMBOLS = ["registerContentFrame"]; + +// This may be an overriden version of the SDK so use the PATH as a key for the +// initial messages before we have a loaderID. +const PATH = __URI__.replace('framescript/content.jsm', ''); + +const { Loader } = Cu.import(PATH + 'toolkit/loader.js', {}); + +// one Loader instance per addon (per @loader/options to be precise) +var addons = new Map(); + +// Tell the parent that a new process is ready +cpmm.sendAsyncMessage('sdk/remote/process/start', { + modulePath: PATH +}); + +// Load a child process module loader with the given loader options +cpmm.addMessageListener('sdk/remote/process/load', ({ data: { modulePath, loaderID, options, reason } }) => { + if (modulePath != PATH) + return; + + // During startup races can mean we get a second load message + if (addons.has(loaderID)) + return; + + options.waiveInterposition = true; + + let loader = Loader.Loader(options); + let addon = { + loader, + require: Loader.Require(loader, { id: 'LoaderHelper' }), + } + addons.set(loaderID, addon); + + cpmm.sendAsyncMessage('sdk/remote/process/attach', { + loaderID, + processID: Services.appinfo.processID, + isRemote: Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + }); + + addon.child = addon.require('sdk/remote/child'); + + for (let contentFrame of frames.values()) + addon.child.registerContentFrame(contentFrame); +}); + +// Unload a child process loader +cpmm.addMessageListener('sdk/remote/process/unload', ({ data: { loaderID, reason } }) => { + if (!addons.has(loaderID)) + return; + + let addon = addons.get(loaderID); + Loader.unload(addon.loader, reason); + + // We want to drop the reference to the loader but never allow creating a new + // loader with the same ID + addons.set(loaderID, {}); +}) + + +var frames = new Set(); + +this.registerContentFrame = contentFrame => { + contentFrame.addEventListener("unload", () => { + unregisterContentFrame(contentFrame); + }, false); + + frames.add(contentFrame); + + for (let addon of addons.values()) { + if ("child" in addon) + addon.child.registerContentFrame(contentFrame); + } +}; + +function unregisterContentFrame(contentFrame) { + frames.delete(contentFrame); + + for (let addon of addons.values()) { + if ("child" in addon) + addon.child.unregisterContentFrame(contentFrame); + } +} diff --git a/addon-sdk/source/lib/framescript/context-menu.js b/addon-sdk/source/lib/framescript/context-menu.js new file mode 100644 index 000000000..3915b7cd8 --- /dev/null +++ b/addon-sdk/source/lib/framescript/context-menu.js @@ -0,0 +1,215 @@ +/* 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 { query, constant, cache } = require("sdk/lang/functional"); +const { pairs, each, map, object } = require("sdk/util/sequence"); +const { nodeToMessageManager } = require("./util"); + +// Decorator function that takes `f` function and returns one that attempts +// to run `f` with given arguments. In case of exception error is logged +// and `fallback` is returned instead. +const Try = (fn, fallback=null) => (...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 000000000..1f261e1fa --- /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 000000000..fb6834608 --- /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 000000000..e0032240a --- /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/jetpack-id/index.js b/addon-sdk/source/lib/jetpack-id/index.js new file mode 100644 index 000000000..6c1493f1d --- /dev/null +++ b/addon-sdk/source/lib/jetpack-id/index.js @@ -0,0 +1,53 @@ +/* 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/. */ + +/** + * Takes parsed `package.json` manifest and returns + * valid add-on id for it. + */ +function getID(manifest) { + manifest = manifest || {}; + + if (manifest.id) { + + if (typeof manifest.id !== "string") { + return null; + } + + // If manifest.id is already valid (as domain or GUID), use it + if (isValidAOMName(manifest.id)) { + return manifest.id; + } + // Otherwise, this ID is invalid so return `null` + return null; + } + + // If no `id` defined, turn `name` into a domain ID, + // as we transition to `name` being an id, similar to node/npm, but + // append a '@' to make it compatible with Firefox requirements + if (manifest.name) { + + if (typeof manifest.name !== "string") { + return null; + } + + var modifiedName = "@" + manifest.name; + return isValidAOMName(modifiedName) ? modifiedName : null; + } + + // If no `id` or `name` property, return null as this manifest + // is invalid + return null; +} + +module.exports = getID; + +/** + * Regex taken from XPIProvider.jsm in the Addon Manager to validate proper + * IDs that are able to be used. + * http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm#209 + */ +function isValidAOMName (s) { + return /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i.test(s || ""); +} diff --git a/addon-sdk/source/lib/jetpack-id/package.json b/addon-sdk/source/lib/jetpack-id/package.json new file mode 100644 index 000000000..62a1c73ba --- /dev/null +++ b/addon-sdk/source/lib/jetpack-id/package.json @@ -0,0 +1,28 @@ +{ + "name": "jetpack-id", + "version": "1.0.0", + "description": "Creates an ID from a Firefox Jetpack manifest", + "main": "index.js", + "repository": { + "type": "git", + "url": "http://github.com/jsantell/jetpack-id" + }, + "author": { + "name": "Jordan Santell", + "url": "http://github.com/jsantell" + }, + "license": "MPL-2.0", + "scripts": { + "test": "./node_modules/.bin/mocha --reporter spec --ui bdd" + }, + "keywords": [ + "jetpack", + "addon", + "mozilla", + "firefox" + ], + "devDependencies": { + "mocha": "*", + "chai": "*" + } +} diff --git a/addon-sdk/source/lib/method/.travis.yml b/addon-sdk/source/lib/method/.travis.yml new file mode 100644 index 000000000..780731a47 --- /dev/null +++ b/addon-sdk/source/lib/method/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - 0.4 + - 0.5 + - 0.6 diff --git a/addon-sdk/source/lib/method/History.md b/addon-sdk/source/lib/method/History.md new file mode 100644 index 000000000..95258c45f --- /dev/null +++ b/addon-sdk/source/lib/method/History.md @@ -0,0 +1,55 @@ +# Changes + +## 1.0.2 / 2012-12-26 + + - Delegate to polymorphic methods from `.define` and `.implement` so, they + can be overidden. + +## 1.0.1 / 2012-11-11 + + - Fix issues with different `Error` types as they all inherit from + `Error`. + +## 1.0.0 / 2012-11-09 + + - Add browser test integration. + - Fix cross-browser incompatibilities & test failures. + - Add support for host objects. + - Add optional `hint` argument for method to ease debugging. + - Remove default implementation at definition time. + +## 0.1.1 / 2012-10-15 + + - Fix regression causing custom type implementation to be stored on objects. + +## 0.1.0 / 2012-10-15 + + - Remove dependency on name module. + - Implement fallback for engines that do not support ES5. + - Add support for built-in type extensions without extending their prototypes. + - Make API for default definitions more intuitive. + Skipping type argument now defines default: + + isFoo.define(function(value) { + return false + }) + + - Make exposed `define` and `implement` polymorphic. + - Removed dev dependency on swank-js. + - Primitive types `string, number, boolean` no longer inherit method + implementations from `Object`. + +## 0.0.3 / 2012-07-17 + + - Remove module boilerplate + +## 0.0.2 / 2012-06-26 + + - Name changes to make it less conflicting with other library conventions. + - Expose function version of `define` & `implement` methods. + - Expose `Null` and `Undefined` object holding implementations for an + associated types. + +## 0.0.1 / 2012-06-25 + + - Initial release diff --git a/addon-sdk/source/lib/method/License.md b/addon-sdk/source/lib/method/License.md new file mode 100644 index 000000000..ed76489a3 --- /dev/null +++ b/addon-sdk/source/lib/method/License.md @@ -0,0 +1,18 @@ +Copyright 2012 Irakli Gozalishvili. All rights reserved. +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. diff --git a/addon-sdk/source/lib/method/Readme.md b/addon-sdk/source/lib/method/Readme.md new file mode 100644 index 000000000..9584c9160 --- /dev/null +++ b/addon-sdk/source/lib/method/Readme.md @@ -0,0 +1,117 @@ +# method + +[![Build Status](https://secure.travis-ci.org/Gozala/method.png)](http://travis-ci.org/Gozala/method) + +Library provides an API for defining polymorphic methods that dispatch on the +first argument type. This provides a powerful way for decouple abstraction +interface definition from an actual implementation per type, without risks +of interference with other libraries. + +### Motivation + + - Provide a high-performance, dynamic polymorphism construct as an + alternative to existing object methods that does not provides any + mechanics for guarding against name conflicts. + - Allow independent extension of types, and implementations of methods + on types, by different parties. + +## Install + + npm install method + +## Use + +```js +var method = require("method") + +// Define `isWatchable` method that can be implemented for any type. +var isWatchable = method("isWatchable") + +// If you call it on any object it will +// throw as nothing implements that method yet. +//isWatchable({}) // => Exception: method is not implemented + +// If you define private method on `Object.prototype` +// all objects will inherit it. +Object.prototype[isWatchable] = function() { + return false; +} + +isWatchable({}) // => false + + +// Although `isWatchable` property above will be enumerable and there for +// may damage some assumbtions made by other libraries. There for it"s +// recomended to use built-in helpers methods that will define extension +// without breaking assumbtions made by other libraries: + +isWatchable.define(Object, function() { return false }) + + +// There are primitive types in JS that won"t inherit methods from Object: +isWatchable(null) // => Exception: method is not implemented + +// One could either implement methods for such types: +isWatchable.define(null, function() { return false }) +isWatchable.define(undefined, function() { return false }) + +// Or simply define default implementation: +isWatchable.define(function() { return false }) + +// Alternatively default implementation may be provided at creation: +isWatchable = method(function() { return false }) + +// Method dispatches on an first argument type. That allows us to create +// new types with an alternative implementations: +function Watchable() {} +isWatchable.define(Watchable, function() { return true }) + +// This will make all `Watchable` instances watchable! +isWatchable(new Watchable()) // => true + +// Arbitrary objects can also be extended to implement given method. For example +// any object can simply made watchable: +function watchable(object) { + return isWatchable.implement(objct, function() { return true }) +} + +isWatchable(watchable({})) // => true + +// Full protocols can be defined with such methods: +var observers = "observers@" + module.filename +var watchers = method("watchers") +var watch = method("watch") +var unwatch = method("unwatch") + +watchers.define(Watchable, function(target) { + return target[observers] || (target[observers] = []) +}) + +watch.define(Watchable, function(target, watcher) { + var observers = watchers(target) + if (observers.indexOf(watcher) < 0) observers.push(watcher) + return target +}) +unwatch.define(Watchable, function(target, watcher) { + var observers = watchers(target) + var index = observers.indexOf(watcher) + if (observers.indexOf(watcher) >= 0) observers.unshift(watcher) + return target +}) + +// Define type Port that inherits form Watchable + +function Port() {} +Port.prototype = Object.create(Watchable.prototype) + +var emit = method("emit") +emit.define(Port, function(port, message) { + watchers(port).slice().forEach(function(watcher) { + watcher(message) + }) +}) + +var p = new Port() +watch(p, console.log) +emit(p, "hello world") // => info: "hello world" +``` diff --git a/addon-sdk/source/lib/method/core.js b/addon-sdk/source/lib/method/core.js new file mode 100644 index 000000000..a6a5261e6 --- /dev/null +++ b/addon-sdk/source/lib/method/core.js @@ -0,0 +1,225 @@ +"use strict"; + +var defineProperty = Object.defineProperty || function(object, name, property) { + object[name] = property.value + return object +} + +// Shortcut for `Object.prototype.toString` for faster access. +var typefy = Object.prototype.toString + +// Map to for jumping from typeof(value) to associated type prefix used +// as a hash in the map of builtin implementations. +var types = { "function": "Object", "object": "Object" } + +// Array is used to save method implementations for the host objects in order +// to avoid extending them with non-primitive values that could cause leaks. +var host = [] +// Hash map is used to save method implementations for builtin types in order +// to avoid extending their prototypes. This also allows to share method +// implementations for types across diff contexts / frames / compartments. +var builtin = {} + +function Primitive() {} +function ObjectType() {} +ObjectType.prototype = new Primitive() +function ErrorType() {} +ErrorType.prototype = new ObjectType() + +var Default = builtin.Default = Primitive.prototype +var Null = builtin.Null = new Primitive() +var Void = builtin.Void = new Primitive() +builtin.String = new Primitive() +builtin.Number = new Primitive() +builtin.Boolean = new Primitive() + +builtin.Object = ObjectType.prototype +builtin.Error = ErrorType.prototype + +builtin.EvalError = new ErrorType() +builtin.InternalError = new ErrorType() +builtin.RangeError = new ErrorType() +builtin.ReferenceError = new ErrorType() +builtin.StopIteration = new ErrorType() +builtin.SyntaxError = new ErrorType() +builtin.TypeError = new ErrorType() +builtin.URIError = new ErrorType() + + +function Method(hint) { + /** + Private Method is a callable private name that dispatches on the first + arguments same named Method: + + method(object, ...rest) => object[method](...rest) + + Optionally hint string may be provided that will be used in generated names + to ease debugging. + + ## Example + + var foo = Method() + + // Implementation for any types + foo.define(function(value, arg1, arg2) { + // ... + }) + + // Implementation for a specific type + foo.define(BarType, function(bar, arg1, arg2) { + // ... + }) + **/ + + // Create an internal unique name if `hint` is provided it is used to + // prefix name to ease debugging. + var name = (hint || "") + "#" + Math.random().toString(32).substr(2) + + function dispatch(value) { + // Method dispatches on type of the first argument. + // If first argument is `null` or `void` associated implementation is + // looked up in the `builtin` hash where implementations for built-ins + // are stored. + var type = null + var method = value === null ? Null[name] : + value === void(0) ? Void[name] : + // Otherwise attempt to use method with a generated private + // `name` that is supposedly in the prototype chain of the + // `target`. + value[name] || + // Otherwise assume it's one of the built-in type instances, + // in which case implementation is stored in a `builtin` hash. + // Attempt to find a implementation for the given built-in + // via constructor name and method name. + ((type = builtin[(value.constructor || "").name]) && + type[name]) || + // Otherwise assume it's a host object. For host objects + // actual method implementations are stored in the `host` + // array and only index for the implementation is stored + // in the host object's prototype chain. This avoids memory + // leaks that otherwise could happen when saving JS objects + // on host object. + host[value["!" + name] || void(0)] || + // Otherwise attempt to lookup implementation for builtins by + // a type of the value. This basically makes sure that all + // non primitive values will delegate to an `Object`. + ((type = builtin[types[typeof(value)]]) && type[name]) + + + // If method implementation for the type is still not found then + // just fallback for default implementation. + method = method || Default[name] + + + // If implementation is still not found (which also means there is no + // default) just throw an error with a descriptive message. + if (!method) throw TypeError("Type does not implements method: " + name) + + // If implementation was found then just delegate. + return method.apply(method, arguments) + } + + // Make `toString` of the dispatch return a private name, this enables + // method definition without sugar: + // + // var method = Method() + // object[method] = function() { /***/ } + dispatch.toString = function toString() { return name } + + // Copy utility methods for convenient API. + dispatch.implement = implementMethod + dispatch.define = defineMethod + + return dispatch +} + +// Create method shortcuts form functions. +var defineMethod = function defineMethod(Type, lambda) { + return define(this, Type, lambda) +} +var implementMethod = function implementMethod(object, lambda) { + return implement(this, object, lambda) +} + +// Define `implement` and `define` polymorphic methods to allow definitions +// and implementations through them. +var implement = Method("implement") +var define = Method("define") + + +function _implement(method, object, lambda) { + /** + Implements `Method` for the given `object` with a provided `implementation`. + Calling `Method` with `object` as a first argument will dispatch on provided + implementation. + **/ + return defineProperty(object, method.toString(), { + enumerable: false, + configurable: false, + writable: false, + value: lambda + }) +} + +function _define(method, Type, lambda) { + /** + Defines `Method` for the given `Type` with a provided `implementation`. + Calling `Method` with a first argument of this `Type` will dispatch on + provided `implementation`. If `Type` is a `Method` default implementation + is defined. If `Type` is a `null` or `undefined` `Method` is implemented + for that value type. + **/ + + // Attempt to guess a type via `Object.prototype.toString.call` hack. + var type = Type && typefy.call(Type.prototype) + + // If only two arguments are passed then `Type` is actually an implementation + // for a default type. + if (!lambda) Default[method] = Type + // If `Type` is `null` or `void` store implementation accordingly. + else if (Type === null) Null[method] = lambda + else if (Type === void(0)) Void[method] = lambda + // If `type` hack indicates built-in type and type has a name us it to + // store a implementation into associated hash. If hash for this type does + // not exists yet create one. + else if (type !== "[object Object]" && Type.name) { + var Bulitin = builtin[Type.name] || (builtin[Type.name] = new ObjectType()) + Bulitin[method] = lambda + } + // If `type` hack indicates an object, that may be either object or any + // JS defined "Class". If name of the constructor is `Object`, assume it's + // built-in `Object` and store implementation accordingly. + else if (Type.name === "Object") + builtin.Object[method] = lambda + // Host objects are pain!!! Every browser does some crazy stuff for them + // So far all browser seem to not implement `call` method for host object + // constructors. If that is a case here, assume it's a host object and + // store implementation in a `host` array and store `index` in the array + // in a `Type.prototype` itself. This avoids memory leaks that could be + // caused by storing JS objects on a host objects. + else if (Type.call === void(0)) { + var index = host.indexOf(lambda) + if (index < 0) index = host.push(lambda) - 1 + // Prefix private name with `!` so it can be dispatched from the method + // without type checks. + implement("!" + method, Type.prototype, index) + } + // If Got that far `Type` is user defined JS `Class`. Define private name + // as hidden property on it's prototype. + else + implement(method, Type.prototype, lambda) +} + +// And provided implementations for a polymorphic equivalents. +_define(define, _define) +_define(implement, _implement) + +// Define exports on `Method` as it's only thing being exported. +Method.implement = implement +Method.define = define +Method.Method = Method +Method.method = Method +Method.builtin = builtin +Method.host = host + +module.exports = Method diff --git a/addon-sdk/source/lib/method/package.json b/addon-sdk/source/lib/method/package.json new file mode 100644 index 000000000..7bb004e28 --- /dev/null +++ b/addon-sdk/source/lib/method/package.json @@ -0,0 +1,41 @@ +{ + "name": "method", + "id": "method", + "version": "1.0.2", + "description": "Functional polymorphic method dispatch", + "keywords": [ + "method", + "dispatch", + "protocol", + "polymorphism", + "type dispatch" + ], + "author": "Irakli Gozalishvili (http://jeditoolkit.com)", + "homepage": "https://github.com/Gozala/method", + "main": "./core.js", + "repository": { + "type": "git", + "url": "https://github.com/Gozala/method.git", + "web": "https://github.com/Gozala/method" + }, + "bugs": { + "url": "http://github.com/Gozala/method/issues/" + }, + "devDependencies": { + "test": "~0.x.0", + "repl-utils": "~2.0.1", + "phantomify": "~0.1.0" + }, + "scripts": { + "test": "npm run test-node && npm run test-browser", + "test-browser": "node ./node_modules/phantomify/bin/cmd.js ./test/browser.js", + "test-node": "node ./test/common.js", + "repl": "node node_modules/repl-utils" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/Gozala/method/License.md" + } + ] +} diff --git a/addon-sdk/source/lib/method/test/browser.js b/addon-sdk/source/lib/method/test/browser.js new file mode 100644 index 000000000..7c8e6cd52 --- /dev/null +++ b/addon-sdk/source/lib/method/test/browser.js @@ -0,0 +1,20 @@ +"use strict"; + +exports["test common"] = require("./common") + +var Method = require("../core") + +exports["test host objects"] = function(assert) { + var isElement = Method("is-element") + isElement.define(function() { return false }) + + isElement.define(Element, function() { return true }) + + assert.notDeepEqual(typeof(Element.prototype[isElement]), "number", + "Host object's prototype is extended with a number value") + + assert.ok(!isElement({}), "object is not an Element") + assert.ok(document.createElement("div"), "Element is an element") +} + +require("test").run(exports) diff --git a/addon-sdk/source/lib/method/test/common.js b/addon-sdk/source/lib/method/test/common.js new file mode 100644 index 000000000..0418c3a23 --- /dev/null +++ b/addon-sdk/source/lib/method/test/common.js @@ -0,0 +1,272 @@ +"use strict"; + +var Method = require("../core") + +function type(value) { + return Object.prototype.toString.call(value). + split(" "). + pop(). + split("]"). + shift(). + toLowerCase() +} + +var values = [ + null, // 0 + undefined, // 1 + Infinity, // 2 + NaN, // 3 + 5, // 4 + {}, // 5 + Object.create({}), // 6 + Object.create(null), // 7 + [], // 8 + /foo/, // 9 + new Date(), // 10 + Function, // 11 + function() {}, // 12 + true, // 13 + false, // 14 + "string" // 15 +] + +function True() { return true } +function False() { return false } + +var trues = values.map(True) +var falses = values.map(False) + +exports["test throws if not implemented"] = function(assert) { + var method = Method("nope") + + assert.throws(function() { + method({}) + }, /not implement/i, "method throws if not implemented") + + assert.throws(function() { + method(null) + }, /not implement/i, "method throws on null") +} + +exports["test all types inherit from default"] = function(assert) { + var isImplemented = Method("isImplemented") + isImplemented.define(function() { return true }) + + values.forEach(function(value) { + assert.ok(isImplemented(value), + type(value) + " inherits deafult implementation") + }) +} + +exports["test default can be implemented later"] = function(assert) { + var isImplemented = Method("isImplemented") + isImplemented.define(function() { + return true + }) + + values.forEach(function(value) { + assert.ok(isImplemented(value), + type(value) + " inherits deafult implementation") + }) +} + +exports["test dispatch not-implemented"] = function(assert) { + var isDefault = Method("isDefault") + values.forEach(function(value) { + assert.throws(function() { + isDefault(value) + }, /not implement/, type(value) + " throws if not implemented") + }) +} + +exports["test dispatch default"] = function(assert) { + var isDefault = Method("isDefault") + + // Implement default + isDefault.define(True) + assert.deepEqual(values.map(isDefault), trues, + "all implementation inherit from default") + +} + +exports["test dispatch null"] = function(assert) { + var isNull = Method("isNull") + + // Implement default + isNull.define(False) + isNull.define(null, True) + assert.deepEqual(values.map(isNull), + [ true ]. + concat(falses.slice(1)), + "only null gets methods defined for null") +} + +exports["test dispatch undefined"] = function(assert) { + var isUndefined = Method("isUndefined") + + // Implement default + isUndefined.define(False) + isUndefined.define(undefined, True) + assert.deepEqual(values.map(isUndefined), + [ false, true ]. + concat(falses.slice(2)), + "only undefined gets methods defined for undefined") +} + +exports["test dispatch object"] = function(assert) { + var isObject = Method("isObject") + + // Implement default + isObject.define(False) + isObject.define(Object, True) + assert.deepEqual(values.map(isObject), + [ false, false, false, false, false ]. + concat(trues.slice(5, 13)). + concat([false, false, false]), + "all values except primitives inherit Object methods") + +} + +exports["test dispatch number"] = function(assert) { + var isNumber = Method("isNumber") + isNumber.define(False) + isNumber.define(Number, True) + + assert.deepEqual(values.map(isNumber), + falses.slice(0, 2). + concat(true, true, true). + concat(falses.slice(5)), + "all numbers inherit from Number method") +} + +exports["test dispatch string"] = function(assert) { + var isString = Method("isString") + isString.define(False) + isString.define(String, True) + + assert.deepEqual(values.map(isString), + falses.slice(0, 15). + concat(true), + "all strings inherit from String method") +} + +exports["test dispatch function"] = function(assert) { + var isFunction = Method("isFunction") + isFunction.define(False) + isFunction.define(Function, True) + + assert.deepEqual(values.map(isFunction), + falses.slice(0, 11). + concat(true, true). + concat(falses.slice(13)), + "all functions inherit from Function method") +} + +exports["test dispatch date"] = function(assert) { + var isDate = Method("isDate") + isDate.define(False) + isDate.define(Date, True) + + assert.deepEqual(values.map(isDate), + falses.slice(0, 10). + concat(true). + concat(falses.slice(11)), + "all dates inherit from Date method") +} + +exports["test dispatch RegExp"] = function(assert) { + var isRegExp = Method("isRegExp") + isRegExp.define(False) + isRegExp.define(RegExp, True) + + assert.deepEqual(values.map(isRegExp), + falses.slice(0, 9). + concat(true). + concat(falses.slice(10)), + "all regexps inherit from RegExp method") +} + +exports["test redefine for descendant"] = function(assert) { + var isFoo = Method("isFoo") + var ancestor = {} + isFoo.implement(ancestor, function() { return true }) + var descendant = Object.create(ancestor) + isFoo.implement(descendant, function() { return false }) + + assert.ok(isFoo(ancestor), "defined on ancestor") + assert.ok(!isFoo(descendant), "overrided for descendant") +} + +exports["test on custom types"] = function(assert) { + function Bar() {} + var isBar = Method("isBar") + + isBar.define(function() { return false }) + isBar.define(Bar, function() { return true }) + + assert.ok(!isBar({}), "object is get's default implementation") + assert.ok(isBar(new Bar()), "Foo type objects get own implementation") + + var isObject = Method("isObject") + isObject.define(function() { return false }) + isObject.define(Object, function() { return true }) + + assert.ok(isObject(new Bar()), "foo inherits implementation from object") + + + isObject.define(Bar, function() { return false }) + + assert.ok(!isObject(new Bar()), + "implementation inherited form object can be overrided") +} + + +exports["test error types"] = function(assert) { + var isError = Method("isError") + isError.define(function() { return false }) + isError.define(Error, function() { return true }) + + assert.ok(isError(Error("boom")), "error is error") + assert.ok(isError(TypeError("boom")), "type error is an error") + assert.ok(isError(EvalError("boom")), "eval error is an error") + assert.ok(isError(RangeError("boom")), "range error is an error") + assert.ok(isError(ReferenceError("boom")), "reference error is an error") + assert.ok(isError(SyntaxError("boom")), "syntax error is an error") + assert.ok(isError(URIError("boom")), "URI error is an error") +} + +exports["test override define polymorphic method"] = function(assert) { + var define = Method.define + var implement = Method.implement + + var fn = Method("fn") + var methods = {} + implement(define, fn, function(method, label, implementation) { + methods[label] = implementation + }) + + function foo() {} + + define(fn, "foo-case", foo) + + assert.equal(methods["foo-case"], foo, "define set property") +} + +exports["test override define via method API"] = function(assert) { + var define = Method.define + var implement = Method.implement + + var fn = Method("fn") + var methods = {} + define.implement(fn, function(method, label, implementation) { + methods[label] = implementation + }) + + function foo() {} + + define(fn, "foo-case", foo) + + assert.equal(methods["foo-case"], foo, "define set property") +} + +require("test").run(exports) diff --git a/addon-sdk/source/lib/mozilla-toolkit-versioning/index.js b/addon-sdk/source/lib/mozilla-toolkit-versioning/index.js new file mode 100644 index 000000000..2f607c880 --- /dev/null +++ b/addon-sdk/source/lib/mozilla-toolkit-versioning/index.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/. */ + +var versionParse = require('./lib/utils').versionParse; + +var COMPARATORS = ['>=', '<=', '>', '<', '=', '~', '^']; + +exports.parse = function (input) { + input = input || ''; + input = input.trim(); + if (!input) + throw new Error('`parse` argument must be a populated string.'); + + // Handle the "*" case + if (input === "*") { + return { min: undefined, max: undefined }; + } + + var inputs = input.split(' '); + var min; + var max; + + // 1.2.3 - 2.3.4 + if (inputs.length === 3 && inputs[1] === '-') { + return { min: inputs[0], max: inputs[2] }; + } + + inputs.forEach(function (input) { + var parsed = parseExpression(input); + var version = parsed.version; + var comparator = parsed.comparator; + + // 1.2.3 + if (inputs.length === 1 && !comparator) + min = max = version; + + // Parse min + if (~comparator.indexOf('>')) { + if (~comparator.indexOf('=')) + min = version; // >=1.2.3 + else + min = increment(version); // >1.2.3 + } + else if (~comparator.indexOf('<')) { + if (~comparator.indexOf('=')) + max = version; // <=1.2.3 + else + max = decrement(version); // <1.2.3 + } + }); + + return { + min: min, + max : max + }; +}; + +function parseExpression (input) { + for (var i = 0; i < COMPARATORS.length; i++) + if (~input.indexOf(COMPARATORS[i])) + return { + comparator: COMPARATORS[i], + version: input.substr(COMPARATORS[i].length) + }; + return { version: input, comparator: '' }; +} + +/** + * Takes a version string ('1.2.3') and returns a version string + * that'll parse as one less than the input string ('1.2.3.-1'). + * + * @param {String} vString + * @return {String} + */ +function decrement (vString) { + return vString + (vString.charAt(vString.length - 1) === '.' ? '' : '.') + '-1'; +} +exports.decrement = decrement; + +/** + * Takes a version string ('1.2.3') and returns a version string + * that'll parse as greater than the input string by the smallest margin + * possible ('1.2.3.1'). + * listed as number-A, string-B, number-C, string-D in + * Mozilla's Toolkit Format. + * https://developer.mozilla.org/en-US/docs/Toolkit_version_format + * + * @param {String} vString + * @return {String} + */ +function increment (vString) { + var match = versionParse(vString); + var a = match[1]; + var b = match[2]; + var c = match[3]; + var d = match[4]; + var lastPos = vString.length - 1; + var lastChar = vString.charAt(lastPos); + + if (!b) { + return vString + (lastChar === '.' ? '' : '.') + '1'; + } + if (!c) { + return vString + '1'; + } + if (!d) { + return vString.substr(0, lastPos) + (++lastChar); + } + return vString.substr(0, lastPos) + String.fromCharCode(lastChar.charCodeAt(0) + 1); +} +exports.increment = increment; diff --git a/addon-sdk/source/lib/mozilla-toolkit-versioning/lib/utils.js b/addon-sdk/source/lib/mozilla-toolkit-versioning/lib/utils.js new file mode 100644 index 000000000..e068085c0 --- /dev/null +++ b/addon-sdk/source/lib/mozilla-toolkit-versioning/lib/utils.js @@ -0,0 +1,15 @@ +/* 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/. */ + +/** + * Breaks up a version string into the 4 components + * defined in: + * https://developer.mozilla.org/en-US/docs/Toolkit_version_format + * @params {String} val + * @return {String} + */ +function versionParse (val) { + return val.match(/^([0-9\.]*)([a-zA-Z]*)([0-9\.]*)([a-zA-Z]*)$/); +} +exports.versionParse = versionParse; diff --git a/addon-sdk/source/lib/mozilla-toolkit-versioning/package.json b/addon-sdk/source/lib/mozilla-toolkit-versioning/package.json new file mode 100644 index 000000000..d9b0424e5 --- /dev/null +++ b/addon-sdk/source/lib/mozilla-toolkit-versioning/package.json @@ -0,0 +1,21 @@ +{ + "name": "mozilla-toolkit-versioning", + "version": "0.0.2", + "description": "Parser for Mozilla's toolkit version format", + "main": "index.js", + "scripts": { + "test": "./node_modules/.bin/mocha --reporter spec --ui bdd" + }, + "repository": { + "type": "git", + "url": "git://github.com/jsantell/mozilla-toolkit-versioning.git" + }, + "author": "Jordan Santell", + "license": "MIT", + "dependencies": {}, + "devDependencies": { + "mocha": "^1.21.4", + "chai": "^1.9.1", + "mozilla-version-comparator": "^1.0.2" + } +} diff --git a/addon-sdk/source/lib/node/os.js b/addon-sdk/source/lib/node/os.js new file mode 100644 index 000000000..1c41a9246 --- /dev/null +++ b/addon-sdk/source/lib/node/os.js @@ -0,0 +1,90 @@ +/* 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 system = require('../sdk/system'); +const runtime = require('../sdk/system/runtime'); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); +const isWindows = system.platform === 'win32'; +const endianness = ((new Uint32Array((new Uint8Array([1,2,3,4])).buffer))[0] === 0x04030201) ? 'LE' : 'BE'; + +XPCOMUtils.defineLazyGetter(this, "oscpu", () => { + try { + return Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu; + } catch (e) { + return ""; + } +}); + +XPCOMUtils.defineLazyGetter(this, "hostname", () => { + try { + // On some platforms (Linux according to try), this service does not exist and fails. + return Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService).myHostName; + } catch (e) { + return ""; + } +}); + +/** + * Returns a path to a temp directory + */ +exports.tmpdir = () => system.pathFor('TmpD'); + +/** + * Returns the endianness of the architecture: either 'LE' or 'BE' + */ +exports.endianness = () => endianness; + +/** + * Returns hostname of the machine + */ +exports.hostname = () => hostname; + +/** + * Name of the OS type + * Possible values: + * https://developer.mozilla.org/en/OS_TARGET + */ +exports.type = () => runtime.OS; + +/** + * Name of the OS Platform in lower case string. + * Possible values: + * https://developer.mozilla.org/en/OS_TARGET + */ +exports.platform = () => system.platform; + +/** + * Type of processor architecture running: + * 'arm', 'ia32', 'x86', 'x64' + */ +exports.arch = () => system.architecture; + +/** + * Returns the operating system release. + */ +exports.release = () => { + let match = oscpu.match(/(\d[\.\d]*)/); + return match && match.length > 1 ? match[1] : oscpu; +}; + +/** + * Returns EOL character for the OS + */ +exports.EOL = isWindows ? '\r\n' : '\n'; + +/** + * Returns [0, 0, 0], as this is not implemented. + */ +exports.loadavg = () => [0, 0, 0]; + +['uptime', 'totalmem', 'freemem', 'cpus'].forEach(method => { + exports[method] = () => { throw new Error('os.' + method + ' is not supported.'); }; +}); 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 000000000..0397d91e5 --- /dev/null +++ b/addon-sdk/source/lib/sdk/addon/bootstrap.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"; + +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) { + return new Promise(resolve => resolve()); + }, + uninstall(addon, reason) { + return new Promise(resolve => { + 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`); + + resolve(); + }); + }, + startup(addon, reasonCode) { + const { id, version, resourceURI: { spec: addonURI } } = addon; + const rootURI = this.mountURI || addonURI; + const reason = REASON[reasonCode]; + const self = this; + + return 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://devtools/", + "./": baseURI + }, readPaths(id)), + manifest: metadata, + metadata: metadata, + modules: { + "@test/options": {}, + }, + noQuit: prefs.get(`extensions.${id}.sdk.test.no-quit`, false) + }); + self.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`; + + // Init the 'sdk/webextension' module from the bootstrap addon parameter. + require("sdk/webextension").initFromBootstrapAddonParam(addon); + + 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) { + this.unmount(); + return this.unload(REASON[code]); + }, + unload(reason) { + return new Promise(resolve => { + const { loader } = this; + if (loader) { + this.loader = null; + unload(loader, reason); + + setTimeout(() => { + for (let uri of Object.keys(loader.sandboxes)) { + let sandbox = loader.sandboxes[uri]; + if (Cu.getClassName(sandbox, true) == "Sandbox") + Cu.nukeSandbox(sandbox); + delete loader.sandboxes[uri]; + delete loader.modules[uri]; + } + + try { + Cu.nukeSandbox(loader.sharedGlobalSandbox); + } catch (e) { + Cu.reportError(e); + } + + resolve(); + }, 1000); + } + else { + resolve(); + } + }); + } +}; +exports.Bootstrap = Bootstrap; diff --git a/addon-sdk/source/lib/sdk/addon/events.js b/addon-sdk/source/lib/sdk/addon/events.js new file mode 100644 index 000000000..45bada6e1 --- /dev/null +++ b/addon-sdk/source/lib/sdk/addon/events.js @@ -0,0 +1,56 @@ +/* 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' +}; + +var { request: hostReq, response: hostRes } = require('./host'); +var { defer: async } = require('../lang/functional'); +var { defer } = require('../core/promise'); +var { emit: emitSync, on, off } = require('../event/core'); +var { uuid } = require('../util/uuid'); +var emit = async(emitSync); + +// Map of IDs to deferreds +var requests = new Map(); + +// May not be necessary to wrap this in `async` +// once promises are async via bug 881047 +var receive = async(function ({data, id, error}) { + let request = requests.get(id); + if (request) { + if (error) request.reject(error); + else request.resolve(clone(data)); + requests.delete(id); + } +}); +on(hostRes, 'data', receive); + +/* + * Send is a helper to be used in client APIs to send + * a request to host + */ +function send (eventName, data) { + let id = uuid(); + let deferred = defer(); + requests.set(id, deferred); + emit(hostReq, 'data', { + id: id, + data: clone(data), + event: eventName + }); + return deferred.promise; +} +exports.send = send; + +/* + * Implement internal structured cloning algorithm in the future? + * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-dom-interfaces.html#internal-structured-cloning-algorithm + */ +function clone (obj) { + return JSON.parse(JSON.stringify(obj || {})); +} diff --git a/addon-sdk/source/lib/sdk/addon/host.js b/addon-sdk/source/lib/sdk/addon/host.js new file mode 100644 index 000000000..91aa0e869 --- /dev/null +++ b/addon-sdk/source/lib/sdk/addon/host.js @@ -0,0 +1,12 @@ +/* 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" +}; + +exports.request = {}; +exports.response = {}; diff --git a/addon-sdk/source/lib/sdk/addon/installer.js b/addon-sdk/source/lib/sdk/addon/installer.js new file mode 100644 index 000000000..bb8cf8d16 --- /dev/null +++ b/addon-sdk/source/lib/sdk/addon/installer.js @@ -0,0 +1,121 @@ +/* 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/. */ + +module.metadata = { + "stability": "experimental" +}; + +const { Cc, Ci, Cu } = require("chrome"); +const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm"); +const { defer } = require("../core/promise"); +const { setTimeout } = require("../timers"); + +/** + * `install` method error codes: + * + * https://developer.mozilla.org/en/Addons/Add-on_Manager/AddonManager#AddonInstall_errors + */ +exports.ERROR_NETWORK_FAILURE = AddonManager.ERROR_NETWORK_FAILURE; +exports.ERROR_INCORRECT_HASH = AddonManager.ERROR_INCORRECT_HASH; +exports.ERROR_CORRUPT_FILE = AddonManager.ERROR_CORRUPT_FILE; +exports.ERROR_FILE_ACCESS = AddonManager.ERROR_FILE_ACCESS; + +/** + * Immediatly install an addon. + * + * @param {String} xpiPath + * file path to an xpi file to install + * @return {Promise} + * A promise resolved when the addon is finally installed. + * Resolved with addon id as value or rejected with an error code. + */ +exports.install = function install(xpiPath) { + let { promise, resolve, reject } = defer(); + + // Create nsIFile for the xpi file + let file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile); + try { + file.initWithPath(xpiPath); + } + catch(e) { + reject(exports.ERROR_FILE_ACCESS); + return promise; + } + + // Listen for installation end + let listener = { + onInstallEnded: function(aInstall, aAddon) { + aInstall.removeListener(listener); + // Bug 749745: on FF14+, onInstallEnded is called just before `startup()` + // is called, but we expect to resolve the promise only after it. + // As startup is called synchronously just after onInstallEnded, + // a simple setTimeout(0) is enough + setTimeout(resolve, 0, aAddon.id); + }, + onInstallFailed: function (aInstall) { + aInstall.removeListener(listener); + reject(aInstall.error); + }, + onDownloadFailed: function(aInstall) { + this.onInstallFailed(aInstall); + } + }; + + // Order AddonManager to install the addon + AddonManager.getInstallForFile(file, function(install) { + if (install.error == 0) { + install.addListener(listener); + install.install(); + } else { + reject(install.error); + } + }); + + return promise; +}; + +exports.uninstall = function uninstall(addonId) { + let { promise, resolve, reject } = defer(); + + // Listen for uninstallation end + let listener = { + onUninstalled: function onUninstalled(aAddon) { + if (aAddon.id != addonId) + return; + AddonManager.removeAddonListener(listener); + resolve(); + } + }; + AddonManager.addAddonListener(listener); + + // Order Addonmanager to uninstall the addon + getAddon(addonId).then(addon => addon.uninstall(), reject); + + return promise; +}; + +exports.disable = function disable(addonId) { + return getAddon(addonId).then(addon => { + addon.userDisabled = true; + return addonId; + }); +}; + +exports.enable = function enabled(addonId) { + return getAddon(addonId).then(addon => { + addon.userDisabled = false; + return addonId; + }); +}; + +exports.isActive = function isActive(addonId) { + return getAddon(addonId).then(addon => addon.isActive && !addon.appDisabled); +}; + +const getAddon = function getAddon (id) { + let { promise, resolve, reject } = defer(); + AddonManager.getAddonByID(id, addon => addon ? resolve(addon) : reject()); + return promise; +} +exports.getAddon = getAddon; diff --git a/addon-sdk/source/lib/sdk/addon/manager.js b/addon-sdk/source/lib/sdk/addon/manager.js new file mode 100644 index 000000000..7ac0a7d6e --- /dev/null +++ b/addon-sdk/source/lib/sdk/addon/manager.js @@ -0,0 +1,18 @@ +/* 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 { AddonManager } = require("resource://gre/modules/AddonManager.jsm"); +const { defer } = require("../core/promise"); + +function getAddonByID(id) { + let { promise, resolve } = defer(); + AddonManager.getAddonByID(id, resolve); + return promise; +} +exports.getAddonByID = getAddonByID; diff --git a/addon-sdk/source/lib/sdk/addon/runner.js b/addon-sdk/source/lib/sdk/addon/runner.js new file mode 100644 index 000000000..3977a04e4 --- /dev/null +++ b/addon-sdk/source/lib/sdk/addon/runner.js @@ -0,0 +1,180 @@ +/* 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/. */ + +module.metadata = { + "stability": "experimental" +}; + +const { Cc, Ci, Cu } = require('chrome'); +const { rootURI, metadata, isNative } = require('@loader/options'); +const { id, loadReason } = require('../self'); +const { descriptor, Sandbox, evaluate, main, resolveURI } = require('toolkit/loader'); +const { once } = require('../system/events'); +const { exit, env, staticArgs } = require('../system'); +const { when: unload } = require('../system/unload'); +const globals = require('../system/globals'); +const xulApp = require('../system/xul-app'); +const { get } = require('../preferences/service'); +const appShellService = Cc['@mozilla.org/appshell/appShellService;1']. + getService(Ci.nsIAppShellService); +const { preferences } = metadata; + +const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function () { + return Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {}). + BrowserToolboxProcess; +}); + +// Initializes default preferences +function setDefaultPrefs(prefsURI) { + const prefs = Cc['@mozilla.org/preferences-service;1']. + getService(Ci.nsIPrefService). + QueryInterface(Ci.nsIPrefBranch2); + const branch = prefs.getDefaultBranch(''); + const sandbox = Sandbox({ + name: prefsURI, + prototype: { + pref: function(key, val) { + switch (typeof val) { + case 'boolean': + branch.setBoolPref(key, val); + break; + case 'number': + if (val % 1 == 0) // number must be a integer, otherwise ignore it + branch.setIntPref(key, val); + break; + case 'string': + branch.setCharPref(key, val); + break; + } + } + } + }); + // load preferences. + evaluate(sandbox, prefsURI); +} + +function definePseudo(loader, id, exports) { + let uri = resolveURI(id, loader.mapping); + loader.modules[uri] = { exports: exports }; +} + +function startup(reason, options) { + return Startup.onceInitialized.then(() => { + // Inject globals ASAP in order to have console API working ASAP + Object.defineProperties(options.loader.globals, descriptor(globals)); + + // NOTE: Module is intentionally required only now because it relies + // on existence of hidden window, which does not exists until startup. + let { ready } = require('../addon/window'); + // Load localization manifest and .properties files. + // Run the addon even in case of error (best effort approach) + require('../l10n/loader'). + load(rootURI). + then(null, function failure(error) { + if (!isNative) + console.info("Error while loading localization: " + error.message); + }). + then(function onLocalizationReady(data) { + // Exports data to a pseudo module so that api-utils/l10n/core + // can get access to it + definePseudo(options.loader, '@l10n/data', data ? data : null); + return ready; + }).then(function() { + run(options); + }).then(null, console.exception); + return void 0; // otherwise we raise a warning, see bug 910304 + }); +} + +function run(options) { + try { + // Try initializing HTML localization before running main module. Just print + // an exception in case of error, instead of preventing addon to be run. + try { + // Do not enable HTML localization while running test as it is hard to + // disable. Because unit tests are evaluated in a another Loader who + // doesn't have access to this current loader. + if (options.main !== 'sdk/test/runner') { + require('../l10n/html').enable(); + } + } + catch(error) { + console.exception(error); + } + + // native-options does stuff directly with preferences key from package.json + if (preferences && preferences.length > 0) { + try { + require('../preferences/native-options'). + enable({ preferences: preferences, id: id }). + catch(console.exception); + } + catch (error) { + console.exception(error); + } + } + else { + // keeping support for addons packaged with older SDK versions, + // when cfx didn't include the 'preferences' key in @loader/options + + // Initialize inline options localization, without preventing addon to be + // run in case of error + try { + require('../l10n/prefs').enable(); + } + catch(error) { + console.exception(error); + } + + // TODO: When bug 564675 is implemented this will no longer be needed + // Always set the default prefs, because they disappear on restart + if (options.prefsURI) { + // Only set if `prefsURI` specified + try { + setDefaultPrefs(options.prefsURI); + } + catch (err) { + // cfx bootstrap always passes prefsURI, even in addons without prefs + } + } + } + + // this is where the addon's main.js finally run. + let program = main(options.loader, options.main); + + if (typeof(program.onUnload) === 'function') + unload(program.onUnload); + + if (typeof(program.main) === 'function') { + program.main({ + loadReason: loadReason, + staticArgs: staticArgs + }, { + print: function print(_) { dump(_ + '\n') }, + quit: exit + }); + } + + if (get("extensions." + id + ".sdk.debug.show", false)) { + BrowserToolboxProcess.init({ addonID: id }); + } + } catch (error) { + console.exception(error); + throw error; + } +} +exports.startup = startup; + +// If add-on is lunched via `cfx run` we need to use `system.exit` to let +// cfx know we're done (`cfx test` will take care of exit so we don't do +// anything here). +if (env.CFX_COMMAND === 'run') { + unload(function(reason) { + if (reason === 'shutdown') + exit(0); + }); +} diff --git a/addon-sdk/source/lib/sdk/addon/window.js b/addon-sdk/source/lib/sdk/addon/window.js new file mode 100644 index 000000000..93ed1d8dc --- /dev/null +++ b/addon-sdk/source/lib/sdk/addon/window.js @@ -0,0 +1,66 @@ +/* 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 { Ci, Cc } = require("chrome"); +const { make: makeWindow, getHiddenWindow } = require("../window/utils"); +const { create: makeFrame, getDocShell } = require("../frame/utils"); +const { defer } = require("../core/promise"); +const { when: unload } = require("../system/unload"); +const cfxArgs = require("../test/options"); + +var addonPrincipal = Cc["@mozilla.org/systemprincipal;1"]. + createInstance(Ci.nsIPrincipal); + +var hiddenWindow = getHiddenWindow(); + +if (cfxArgs.parseable) { + console.info("hiddenWindow document.documentURI:" + + hiddenWindow.document.documentURI); + console.info("hiddenWindow document.readyState:" + + hiddenWindow.document.readyState); +} + +// Once Bug 565388 is fixed and shipped we'll be able to make invisible, +// permanent docShells. Meanwhile we create hidden top level window and +// use it's docShell. +var frame = makeFrame(hiddenWindow.document, { + nodeName: "iframe", + namespaceURI: "http://www.w3.org/1999/xhtml", + allowJavascript: true, + allowPlugins: true +}) +var docShell = getDocShell(frame); +var eventTarget = docShell.chromeEventHandler; + +// We need to grant docShell system principals in order to load XUL document +// from data URI into it. +docShell.createAboutBlankContentViewer(addonPrincipal); + +// Get a reference to the DOM window of the given docShell and load +// such document into that would allow us to create XUL iframes, that +// are necessary for hidden frames etc.. +var window = docShell.contentViewer.DOMDocument.defaultView; +window.location = "data:application/vnd.mozilla.xul+xml;charset=utf-8,"; + +// Create a promise that is delivered once add-on window is interactive, +// used by add-on runner to defer add-on loading until window is ready. +var { promise, resolve } = defer(); +eventTarget.addEventListener("DOMContentLoaded", function handler(event) { + eventTarget.removeEventListener("DOMContentLoaded", handler, false); + resolve(); +}, false); + +exports.ready = promise; +exports.window = window; + +// Still close window on unload to claim memory back early. +unload(function() { + window.close() + frame.parentNode.removeChild(frame); +}); diff --git a/addon-sdk/source/lib/sdk/base64.js b/addon-sdk/source/lib/sdk/base64.js new file mode 100644 index 000000000..a07b302e0 --- /dev/null +++ b/addon-sdk/source/lib/sdk/base64.js @@ -0,0 +1,47 @@ +/* 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 { Cu } = require("chrome"); + +// Passing an empty object as second argument to avoid scope's pollution +// (devtools loader injects these symbols as global and prevent using +// const here) +var { atob, btoa } = Cu.import("resource://gre/modules/Services.jsm", {}); + +function isUTF8(charset) { + let type = typeof charset; + + if (type === "undefined") + return false; + + if (type === "string" && charset.toLowerCase() === "utf-8") + return true; + + throw new Error("The charset argument can be only 'utf-8'"); +} + +function toOctetChar(c) { + return String.fromCharCode(c.charCodeAt(0) & 0xFF); +} + +exports.decode = function (data, charset) { + if (isUTF8(charset)) + return decodeURIComponent(escape(atob(data))) + + return atob(data); +} + +exports.encode = function (data, charset) { + if (isUTF8(charset)) + return btoa(unescape(encodeURIComponent(data))) + + data = data.replace(/[^\x00-\xFF]/g, toOctetChar); + return btoa(data); +} diff --git a/addon-sdk/source/lib/sdk/browser/events.js b/addon-sdk/source/lib/sdk/browser/events.js new file mode 100644 index 000000000..f91119031 --- /dev/null +++ b/addon-sdk/source/lib/sdk/browser/events.js @@ -0,0 +1,20 @@ +/* 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 { events } = require("../window/events"); +const { filter } = require("../event/utils"); +const { isBrowser } = require("../window/utils"); + +// TODO: `isBrowser` detects weather window is a browser by checking +// `windowtype` attribute, which means that all 'open' events will be +// filtered out since document is not loaded yet. Maybe we can find a better +// implementation for `isBrowser`. Either way it's not really needed yet +// neither window tracker provides this event. + +exports.events = filter(events, ({target}) => isBrowser(target)); diff --git a/addon-sdk/source/lib/sdk/clipboard.js b/addon-sdk/source/lib/sdk/clipboard.js new file mode 100644 index 000000000..048d5f2f1 --- /dev/null +++ b/addon-sdk/source/lib/sdk/clipboard.js @@ -0,0 +1,337 @@ +/* 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": "stable", + "engines": { + // TODO Fennec Support 789757 + "Firefox": "*", + "SeaMonkey": "*", + "Thunderbird": "*" + } +}; + +const { Cc, Ci } = require("chrome"); +const { DataURL } = require("./url"); +const apiUtils = require("./deprecated/api-utils"); +/* +While these data flavors resemble Internet media types, they do +no directly map to them. +*/ +const kAllowableFlavors = [ + "text/unicode", + "text/html", + "image/png" + /* CURRENTLY UNSUPPORTED FLAVORS + "text/plain", + "image/jpg", + "image/jpeg", + "image/gif", + "text/x-moz-text-internal", + "AOLMAIL", + "application/x-moz-file", + "text/x-moz-url", + "text/x-moz-url-data", + "text/x-moz-url-desc", + "text/x-moz-url-priv", + "application/x-moz-nativeimage", + "application/x-moz-nativehtml", + "application/x-moz-file-promise-url", + "application/x-moz-file-promise-dest-filename", + "application/x-moz-file-promise", + "application/x-moz-file-promise-dir" + */ +]; + +/* +Aliases for common flavors. Not all flavors will +get an alias. New aliases must be approved by a +Jetpack API druid. +*/ +const kFlavorMap = [ + { short: "text", long: "text/unicode" }, + { short: "html", long: "text/html" }, + { short: "image", long: "image/png" } +]; + +var clipboardService = Cc["@mozilla.org/widget/clipboard;1"]. + getService(Ci.nsIClipboard); + +var clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + +var imageTools = Cc["@mozilla.org/image/tools;1"]. + getService(Ci.imgITools); + +exports.set = function(aData, aDataType) { + + let options = { + data: aData, + datatype: aDataType || "text" + }; + + // If `aDataType` is not given or if it's "image", the data is parsed as + // data URL to detect a better datatype + if (aData && (!aDataType || aDataType === "image")) { + try { + let dataURL = new DataURL(aData); + + options.datatype = dataURL.mimeType; + options.data = dataURL.data; + } + catch (e) { + // Ignore invalid URIs + if (e.name !== "URIError") { + throw e; + } + } + } + + options = apiUtils.validateOptions(options, { + data: { + is: ["string"] + }, + datatype: { + is: ["string"] + } + }); + + let flavor = fromJetpackFlavor(options.datatype); + + if (!flavor) + throw new Error("Invalid flavor for " + options.datatype); + + // Additional checks for using the simple case + if (flavor == "text/unicode") { + clipboardHelper.copyString(options.data); + return true; + } + + // Below are the more complex cases where we actually have to work with a + // nsITransferable object + var xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + if (!xferable) + throw new Error("Couldn't set the clipboard due to an internal error " + + "(couldn't create a Transferable object)."); + // Bug 769440: Starting with FF16, transferable have to be inited + if ("init" in xferable) + xferable.init(null); + + 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); + + // add a text/unicode flavor (html converted to plain text) + 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 + // it works on platform side, see: + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530 + case "image/png": + let image = options.data; + + let container = {}; + + try { + let input = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + + input.setData(image, image.length); + + imageTools.decodeImageData(input, flavor, container); + } + catch (e) { + throw new Error("Unable to decode data given in a valid image."); + } + + // Store directly the input stream makes the cliboard's data available + // for Firefox but not to the others application or to the OS. Therefore, + // a `nsISupportsInterfacePointer` object that reference an `imgIContainer` + // with the image is needed. + var imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"]. + createInstance(Ci.nsISupportsInterfacePointer); + + imgPtr.data = container.value; + + xferable.addDataFlavor(flavor); + xferable.setTransferData(flavor, imgPtr, -1); + + break; + default: + throw new Error("Unable to handle the flavor " + flavor + "."); + } + + // TODO: Not sure if this will ever actually throw. -zpao + try { + clipboardService.setData( + xferable, + null, + clipboardService.kGlobalClipboard + ); + } catch (e) { + throw new Error("Couldn't set clipboard data due to an internal error: " + e); + } + return true; +}; + + +exports.get = function(aDataType) { + let options = { + datatype: aDataType + }; + + // Figure out the best data type for the clipboard's data, if omitted + if (!aDataType) { + if (~currentFlavors().indexOf("image")) + options.datatype = "image"; + else + options.datatype = "text"; + } + + options = apiUtils.validateOptions(options, { + datatype: { + is: ["string"] + } + }); + + var xferable = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + if (!xferable) + throw new Error("Couldn't set the clipboard due to an internal error " + + "(couldn't create a Transferable object)."); + // Bug 769440: Starting with FF16, transferable have to be inited + if ("init" in xferable) + xferable.init(null); + + var flavor = fromJetpackFlavor(options.datatype); + + // Ensure that the user hasn't requested a flavor that we don't support. + if (!flavor) + throw new Error("Getting the clipboard with the flavor '" + flavor + + "' is not supported."); + + // TODO: Check for matching flavor first? Probably not worth it. + + xferable.addDataFlavor(flavor); + // Get the data into our transferable. + clipboardService.getData( + xferable, + clipboardService.kGlobalClipboard + ); + + var data = {}; + var dataLen = {}; + try { + xferable.getTransferData(flavor, data, dataLen); + } catch (e) { + // Clipboard doesn't contain data in flavor, return null. + return null; + } + + // There's no data available, return. + if (data.value === null) + return null; + + // TODO: Add flavors here as we support more in kAllowableFlavors. + switch (flavor) { + case "text/unicode": + case "text/html": + data = data.value.QueryInterface(Ci.nsISupportsString).data; + break; + case "image/png": + let dataURL = new DataURL(); + + dataURL.mimeType = flavor; + dataURL.base64 = true; + + let image = data.value; + + // Due to the differences in how images could be stored in the clipboard + // the checks below are needed. The clipboard could already provide the + // image as byte streams, but also as pointer, or as image container. + // If it's not possible obtain a byte stream, the function returns `null`. + if (image instanceof Ci.nsISupportsInterfacePointer) + image = image.data; + + if (image instanceof Ci.imgIContainer) + image = imageTools.encodeImage(image, flavor); + + if (image instanceof Ci.nsIInputStream) { + let binaryStream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + + binaryStream.setInputStream(image); + + dataURL.data = binaryStream.readBytes(binaryStream.available()); + + data = dataURL.toString(); + } + else + data = null; + + break; + default: + data = null; + } + + return data; +}; + +function currentFlavors() { + // Loop over kAllowableFlavors, calling hasDataMatchingFlavors for each. + // This doesn't seem like the most efficient way, but we can't get + // confirmation for specific flavors any other way. This is supposed to be + // an inexpensive call, so performance shouldn't be impacted (much). + var currentFlavors = []; + for (var flavor of kAllowableFlavors) { + var matches = clipboardService.hasDataMatchingFlavors( + [flavor], + 1, + clipboardService.kGlobalClipboard + ); + if (matches) + currentFlavors.push(toJetpackFlavor(flavor)); + } + return currentFlavors; +}; + +Object.defineProperty(exports, "currentFlavors", { get : currentFlavors }); + +// SUPPORT FUNCTIONS //////////////////////////////////////////////////////// + +function toJetpackFlavor(aFlavor) { + for (let flavorMap of kFlavorMap) + if (flavorMap.long == aFlavor) + return flavorMap.short; + // Return null in the case where we don't match + return null; +} + +function fromJetpackFlavor(aJetpackFlavor) { + // TODO: Handle proper flavors better + for (let flavorMap of kFlavorMap) + if (flavorMap.short == aJetpackFlavor || flavorMap.long == aJetpackFlavor) + return flavorMap.long; + // Return null in the case where we don't match. + return null; +} diff --git a/addon-sdk/source/lib/sdk/console/plain-text.js b/addon-sdk/source/lib/sdk/console/plain-text.js new file mode 100644 index 000000000..0e44cf106 --- /dev/null +++ b/addon-sdk/source/lib/sdk/console/plain-text.js @@ -0,0 +1,78 @@ +/* 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, Cu, Cr } = require("chrome"); +const self = require("../self"); +const prefs = require("../preferences/service"); +const { merge } = require("../util/object"); +const { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {}); + +const DEFAULT_LOG_LEVEL = "error"; +const ADDON_LOG_LEVEL_PREF = "extensions." + self.id + ".sdk.console.logLevel"; +const SDK_LOG_LEVEL_PREF = "extensions.sdk.console.logLevel"; + +var logLevel = DEFAULT_LOG_LEVEL; +function setLogLevel() { + logLevel = prefs.get(ADDON_LOG_LEVEL_PREF, + prefs.get(SDK_LOG_LEVEL_PREF, + DEFAULT_LOG_LEVEL)); +} +setLogLevel(); + +var logLevelObserver = { + QueryInterface: function(iid) { + if (!iid.equals(Ci.nsIObserver) && + !iid.equals(Ci.nsISupportsWeakReference) && + !iid.equals(Ci.nsISupports)) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + }, + observe: function(subject, topic, data) { + setLogLevel(); + } +}; +var branch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(null); +branch.addObserver(ADDON_LOG_LEVEL_PREF, logLevelObserver, true); +branch.addObserver(SDK_LOG_LEVEL_PREF, logLevelObserver, true); + +function PlainTextConsole(print, innerID) { + + let consoleOptions = { + prefix: self.name, + maxLogLevel: logLevel, + dump: print, + innerID: innerID, + consoleID: "addon/" + self.id + }; + let console = new ConsoleAPI(consoleOptions); + + // As we freeze the console object, we can't modify this property afterward + Object.defineProperty(console, "maxLogLevel", { + get: function() { + return logLevel; + } + }); + + // We defined the `__exposedProps__` in our console chrome object. + // + // Meanwhile we're investigating with the platform team if `__exposedProps__` + // are needed, or are just a left-over. + + console.__exposedProps__ = Object.keys(ConsoleAPI.prototype).reduce(function(exposed, prop) { + exposed[prop] = "r"; + return exposed; + }, {}); + + Object.freeze(console); + return console; +}; +exports.PlainTextConsole = PlainTextConsole; diff --git a/addon-sdk/source/lib/sdk/console/traceback.js b/addon-sdk/source/lib/sdk/console/traceback.js new file mode 100644 index 000000000..be0fb7b94 --- /dev/null +++ b/addon-sdk/source/lib/sdk/console/traceback.js @@ -0,0 +1,86 @@ +/* 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 { Ci, components } = require("chrome"); +const { parseStack, sourceURI } = require("toolkit/loader"); +const { readURISync } = require("../net/url"); + +function safeGetFileLine(path, line) { + try { + var scheme = require("../url").URL(path).scheme; + // TODO: There should be an easier, more accurate way to figure out + // what's the case here. + if (!(scheme == "http" || scheme == "https")) + return readURISync(path).split("\n")[line - 1]; + } catch (e) {} + return null; +} + +function nsIStackFramesToJSON(frame) { + var stack = []; + + while (frame) { + if (frame.filename) { + stack.unshift({ + fileName: sourceURI(frame.filename), + lineNumber: frame.lineNumber, + name: frame.name + }); + } + frame = frame.caller; + } + + return stack; +}; + +var fromException = exports.fromException = function fromException(e) { + if (e instanceof Ci.nsIException) + return nsIStackFramesToJSON(e.location); + if (e.stack && e.stack.length) + return parseStack(e.stack); + if (e.fileName && typeof(e.lineNumber == "number")) + return [{fileName: sourceURI(e.fileName), + lineNumber: e.lineNumber, + name: null}]; + return []; +}; + +var get = exports.get = function get() { + return nsIStackFramesToJSON(components.stack.caller); +}; + +var format = exports.format = function format(tbOrException) { + if (tbOrException === undefined) { + tbOrException = get(); + tbOrException.pop(); + } + + var tb; + if (typeof(tbOrException) == "object" && + tbOrException.constructor.name == "Array") + tb = tbOrException; + else + tb = fromException(tbOrException); + + var lines = ["Traceback (most recent call last):"]; + + tb.forEach( + function(frame) { + if (!(frame.fileName || frame.lineNumber || frame.name)) + return; + + lines.push(' File "' + frame.fileName + '", line ' + + frame.lineNumber + ', in ' + frame.name); + var sourceLine = safeGetFileLine(frame.fileName, frame.lineNumber); + if (sourceLine) + lines.push(' ' + sourceLine.trim()); + }); + + return lines.join("\n"); +}; diff --git a/addon-sdk/source/lib/sdk/content/content-worker.js b/addon-sdk/source/lib/sdk/content/content-worker.js new file mode 100644 index 000000000..0a8225733 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/content-worker.js @@ -0,0 +1,305 @@ +/* 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/. */ + +Object.freeze({ + // TODO: Bug 727854 Use same implementation than common JS modules, + // i.e. EventEmitter module + + /** + * Create an EventEmitter instance. + */ + createEventEmitter: function createEventEmitter(emit) { + let listeners = Object.create(null); + let eventEmitter = Object.freeze({ + emit: emit, + on: function on(name, callback) { + if (typeof callback !== "function") + return this; + if (!(name in listeners)) + listeners[name] = []; + listeners[name].push(callback); + return this; + }, + once: function once(name, callback) { + eventEmitter.on(name, function onceCallback() { + eventEmitter.removeListener(name, onceCallback); + callback.apply(callback, arguments); + }); + }, + removeListener: function removeListener(name, callback) { + if (!(name in listeners)) + return; + let index = listeners[name].indexOf(callback); + if (index == -1) + return; + listeners[name].splice(index, 1); + } + }); + function onEvent(name) { + if (!(name in listeners)) + return []; + let args = Array.slice(arguments, 1); + let results = []; + for (let callback of listeners[name]) { + results.push(callback.apply(null, args)); + } + return results; + } + return { + eventEmitter: eventEmitter, + emit: onEvent + }; + }, + + /** + * Create an EventEmitter instance to communicate with chrome module + * by passing only strings between compartments. + * This function expects `emitToChrome` function, that allows to send + * events to the chrome module. It returns the EventEmitter as `pipe` + * attribute, and, `onChromeEvent` a function that allows chrome module + * to send event into the EventEmitter. + * + * pipe.emit --> emitToChrome + * onChromeEvent --> callback registered through pipe.on + */ + createPipe: function createPipe(emitToChrome) { + let ContentWorker = this; + function onEvent(type, ...args) { + // JSON.stringify is buggy with cross-sandbox values, + // it may return "{}" on functions. Use a replacer to match them correctly. + let replacer = (k, v) => + typeof(v) === "function" + ? (type === "console" ? Function.toString.call(v) : void(0)) + : v; + + let str = JSON.stringify([type, ...args], replacer); + emitToChrome(str); + } + + let { eventEmitter, emit } = + ContentWorker.createEventEmitter(onEvent); + + return { + pipe: eventEmitter, + onChromeEvent: function onChromeEvent(array) { + // We either receive a stringified array, or a real array. + // We still allow to pass an array of objects, in WorkerSandbox.emitSync + // in order to allow sending DOM node reference between content script + // and modules (only used for context-menu API) + let args = typeof array == "string" ? JSON.parse(array) : array; + return emit.apply(null, args); + } + }; + }, + + injectConsole: function injectConsole(exports, pipe) { + exports.console = Object.freeze({ + log: pipe.emit.bind(null, "console", "log"), + info: pipe.emit.bind(null, "console", "info"), + warn: pipe.emit.bind(null, "console", "warn"), + error: pipe.emit.bind(null, "console", "error"), + debug: pipe.emit.bind(null, "console", "debug"), + exception: pipe.emit.bind(null, "console", "exception"), + trace: pipe.emit.bind(null, "console", "trace"), + time: pipe.emit.bind(null, "console", "time"), + timeEnd: pipe.emit.bind(null, "console", "timeEnd") + }); + }, + + injectTimers: function injectTimers(exports, chromeAPI, pipe, console) { + // wrapped functions from `'timer'` module. + // Wrapper adds `try catch` blocks to the callbacks in order to + // emit `error` event if exception is thrown in + // the Worker global scope. + // @see http://www.w3.org/TR/workers/#workerutils + + // List of all living timeouts/intervals + let _timers = Object.create(null); + + // Keep a reference to original timeout functions + let { + setTimeout: chromeSetTimeout, + setInterval: chromeSetInterval, + clearTimeout: chromeClearTimeout, + clearInterval: chromeClearInterval + } = chromeAPI.timers; + + function registerTimer(timer) { + let registerMethod = null; + if (timer.kind == "timeout") + registerMethod = chromeSetTimeout; + else if (timer.kind == "interval") + registerMethod = chromeSetInterval; + else + throw new Error("Unknown timer kind: " + timer.kind); + + if (typeof timer.fun == 'string') { + let code = timer.fun; + timer.fun = () => chromeAPI.sandbox.evaluate(exports, code); + } else if (typeof timer.fun != 'function') { + throw new Error('Unsupported callback type' + typeof timer.fun); + } + + let id = registerMethod(onFire, timer.delay); + function onFire() { + try { + if (timer.kind == "timeout") + delete _timers[id]; + timer.fun.apply(null, timer.args); + } catch(e) { + console.exception(e); + let wrapper = { + instanceOfError: instanceOf(e, Error), + value: e, + }; + if (wrapper.instanceOfError) { + wrapper.value = { + message: e.message, + fileName: e.fileName, + lineNumber: e.lineNumber, + stack: e.stack, + name: e.name, + }; + } + pipe.emit('error', wrapper); + } + } + _timers[id] = timer; + return id; + } + + // copied from sdk/lang/type.js since modules are not available here + function instanceOf(value, Type) { + var isConstructorNameSame; + var isConstructorSourceSame; + + // If `instanceof` returned `true` we know result right away. + var isInstanceOf = value instanceof Type; + + // If `instanceof` returned `false` we do ducktype check since `Type` may be + // from a different sandbox. If a constructor of the `value` or a constructor + // of the value's prototype has same name and source we assume that it's an + // instance of the Type. + if (!isInstanceOf && value) { + isConstructorNameSame = value.constructor.name === Type.name; + isConstructorSourceSame = String(value.constructor) == String(Type); + isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) || + instanceOf(Object.getPrototypeOf(value), Type); + } + return isInstanceOf; + } + + function unregisterTimer(id) { + if (!(id in _timers)) + return; + let { kind } = _timers[id]; + delete _timers[id]; + if (kind == "timeout") + chromeClearTimeout(id); + else if (kind == "interval") + chromeClearInterval(id); + else + throw new Error("Unknown timer kind: " + kind); + } + + function disableAllTimers() { + Object.keys(_timers).forEach(unregisterTimer); + } + + exports.setTimeout = function ContentScriptSetTimeout(callback, delay) { + return registerTimer({ + kind: "timeout", + fun: callback, + delay: delay, + args: Array.slice(arguments, 2) + }); + }; + exports.clearTimeout = function ContentScriptClearTimeout(id) { + unregisterTimer(id); + }; + + exports.setInterval = function ContentScriptSetInterval(callback, delay) { + return registerTimer({ + kind: "interval", + fun: callback, + delay: delay, + args: Array.slice(arguments, 2) + }); + }; + exports.clearInterval = function ContentScriptClearInterval(id) { + unregisterTimer(id); + }; + + // On page-hide, save a list of all existing timers before disabling them, + // in order to be able to restore them on page-show. + // These events are fired when the page goes in/out of bfcache. + // https://developer.mozilla.org/En/Working_with_BFCache + let frozenTimers = []; + pipe.on("pageshow", function onPageShow() { + frozenTimers.forEach(registerTimer); + }); + pipe.on("pagehide", function onPageHide() { + frozenTimers = []; + for (let id in _timers) + frozenTimers.push(_timers[id]); + disableAllTimers(); + // Some other pagehide listeners may register some timers that won't be + // frozen as this particular pagehide listener is called first. + // So freeze these timers on next cycle. + chromeSetTimeout(function () { + for (let id in _timers) + frozenTimers.push(_timers[id]); + disableAllTimers(); + }, 0); + }); + + // Unregister all timers when the page is destroyed + // (i.e. when it is removed from bfcache) + pipe.on("detach", function clearTimeouts() { + disableAllTimers(); + _timers = {}; + frozenTimers = []; + }); + }, + + injectMessageAPI: function injectMessageAPI(exports, pipe, console) { + + let ContentWorker = this; + let { eventEmitter: port, emit : portEmit } = + ContentWorker.createEventEmitter(pipe.emit.bind(null, "event")); + pipe.on("event", portEmit); + + let self = { + port: port, + postMessage: pipe.emit.bind(null, "message"), + on: pipe.on.bind(null), + once: pipe.once.bind(null), + removeListener: pipe.removeListener.bind(null), + }; + Object.defineProperty(exports, "self", { + value: self + }); + }, + + injectOptions: function (exports, options) { + Object.defineProperty( exports.self, "options", { value: JSON.parse( options ) }); + }, + + inject: function (exports, chromeAPI, emitToChrome, options) { + let ContentWorker = this; + let { pipe, onChromeEvent } = + ContentWorker.createPipe(emitToChrome); + + ContentWorker.injectConsole(exports, pipe); + ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console); + ContentWorker.injectMessageAPI(exports, pipe, exports.console); + if ( options !== undefined ) { + ContentWorker.injectOptions(exports, options); + } + + Object.freeze( exports.self ); + + return onChromeEvent; + } +}); diff --git a/addon-sdk/source/lib/sdk/content/content.js b/addon-sdk/source/lib/sdk/content/content.js new file mode 100644 index 000000000..9655223a3 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/content.js @@ -0,0 +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"; + +module.metadata = { + "stability": "deprecated" +}; + +const { deprecateUsage } = require('../util/deprecate'); + +Object.defineProperty(exports, "Worker", { + get: function() { + deprecateUsage('`sdk/content/content` is deprecated. Please use `sdk/content/worker` directly.'); + return require('./worker').Worker; + } +}); diff --git a/addon-sdk/source/lib/sdk/content/context-menu.js b/addon-sdk/source/lib/sdk/content/context-menu.js new file mode 100644 index 000000000..2955e2f09 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/context-menu.js @@ -0,0 +1,408 @@ +/* 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 } = require("../core/heritage"); +const self = require("../self"); +const { WorkerChild } = require("./worker-child"); +const { getInnerId } = require("../window/utils"); +const { Ci } = require("chrome"); +const { Services } = require("resource://gre/modules/Services.jsm"); +const system = require('../system/events'); +const { process } = require('../remote/child'); + +// These functions are roughly copied from sdk/selection which doesn't work +// in the content process +function getElementWithSelection(window) { + let element = Services.focus.getFocusedElementForWindow(window, false, {}); + if (!element) + return null; + + try { + // Accessing selectionStart and selectionEnd on e.g. a button + // results in an exception thrown as per the HTML5 spec. See + // http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#textFieldSelection + + let { value, selectionStart, selectionEnd } = element; + + let hasSelection = typeof value === "string" && + !isNaN(selectionStart) && + !isNaN(selectionEnd) && + selectionStart !== selectionEnd; + + return hasSelection ? element : null; + } + catch (err) { + console.exception(err); + return null; + } +} + +function safeGetRange(selection, rangeNumber) { + try { + let { rangeCount } = selection; + let range = null; + + for (let rangeNumber = 0; rangeNumber < rangeCount; rangeNumber++ ) { + range = selection.getRangeAt(rangeNumber); + + if (range && range.toString()) + break; + + range = null; + } + + return range; + } + catch (e) { + return null; + } +} + +function getSelection(window) { + let selection = window.getSelection(); + let range = safeGetRange(selection); + if (range) + return range.toString(); + + let node = getElementWithSelection(window); + if (!node) + return null; + + return node.value.substring(node.selectionStart, node.selectionEnd); +} + +//These are used by PageContext.isCurrent below. If the popupNode or any of +//its ancestors is one of these, Firefox uses a tailored context menu, and so +//the page context doesn't apply. +const NON_PAGE_CONTEXT_ELTS = [ + Ci.nsIDOMHTMLAnchorElement, + Ci.nsIDOMHTMLAppletElement, + Ci.nsIDOMHTMLAreaElement, + Ci.nsIDOMHTMLButtonElement, + Ci.nsIDOMHTMLCanvasElement, + Ci.nsIDOMHTMLEmbedElement, + Ci.nsIDOMHTMLImageElement, + Ci.nsIDOMHTMLInputElement, + Ci.nsIDOMHTMLMapElement, + Ci.nsIDOMHTMLMediaElement, + Ci.nsIDOMHTMLMenuElement, + Ci.nsIDOMHTMLObjectElement, + Ci.nsIDOMHTMLOptionElement, + Ci.nsIDOMHTMLSelectElement, + Ci.nsIDOMHTMLTextAreaElement, +]; + +// List all editable types of inputs. Or is it better to have a list +// of non-editable inputs? +var editableInputs = { + email: true, + number: true, + password: true, + search: true, + tel: true, + text: true, + textarea: true, + url: true +}; + +var CONTEXTS = {}; + +var Context = Class({ + initialize: function(id) { + this.id = id; + }, + + adjustPopupNode: function adjustPopupNode(popupNode) { + return popupNode; + }, + + // Gets state to pass through to the parent process for the node the user + // clicked on + getState: function(popupNode) { + return false; + } +}); + +// Matches when the context-clicked node doesn't have any of +// NON_PAGE_CONTEXT_ELTS in its ancestors +CONTEXTS.PageContext = Class({ + extends: Context, + + getState: function(popupNode) { + // If there is a selection in the window then this context does not match + if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed) + return false; + + // If the clicked node or any of its ancestors is one of the blocked + // NON_PAGE_CONTEXT_ELTS then this context does not match + while (!(popupNode instanceof Ci.nsIDOMDocument)) { + if (NON_PAGE_CONTEXT_ELTS.some(type => popupNode instanceof type)) + return false; + + popupNode = popupNode.parentNode; + } + + return true; + } +}); + +// Matches when there is an active selection in the window +CONTEXTS.SelectionContext = Class({ + extends: Context, + + getState: function(popupNode) { + if (!popupNode.ownerDocument.defaultView.getSelection().isCollapsed) + return true; + + try { + // The node may be a text box which has selectionStart and selectionEnd + // properties. If not this will throw. + let { selectionStart, selectionEnd } = popupNode; + return !isNaN(selectionStart) && !isNaN(selectionEnd) && + selectionStart !== selectionEnd; + } + catch (e) { + return false; + } + } +}); + +// Matches when the context-clicked node or any of its ancestors matches the +// selector given +CONTEXTS.SelectorContext = Class({ + extends: Context, + + initialize: function initialize(id, selector) { + Context.prototype.initialize.call(this, id); + this.selector = selector; + }, + + adjustPopupNode: function adjustPopupNode(popupNode) { + let selector = this.selector; + + while (!(popupNode instanceof Ci.nsIDOMDocument)) { + if (popupNode.matches(selector)) + return popupNode; + + popupNode = popupNode.parentNode; + } + + return null; + }, + + getState: function(popupNode) { + return !!this.adjustPopupNode(popupNode); + } +}); + +// Matches when the page url matches any of the patterns given +CONTEXTS.URLContext = Class({ + extends: Context, + + getState: function(popupNode) { + return popupNode.ownerDocument.URL; + } +}); + +// Matches when the user-supplied predicate returns true +CONTEXTS.PredicateContext = Class({ + extends: Context, + + getState: function(node) { + let window = node.ownerDocument.defaultView; + let data = {}; + + data.documentType = node.ownerDocument.contentType; + + data.documentURL = node.ownerDocument.location.href; + data.targetName = node.nodeName.toLowerCase(); + data.targetID = node.id || null ; + + if ((data.targetName === 'input' && editableInputs[node.type]) || + data.targetName === 'textarea') { + data.isEditable = !node.readOnly && !node.disabled; + } + else { + data.isEditable = node.isContentEditable; + } + + data.selectionText = getSelection(window, "TEXT"); + + data.srcURL = node.src || null; + data.value = node.value || null; + + while (!data.linkURL && node) { + data.linkURL = node.href || null; + node = node.parentNode; + } + + return data; + }, +}); + +function instantiateContext({ id, type, args }) { + if (!(type in CONTEXTS)) { + console.error("Attempt to use unknown context " + type); + return; + } + return new CONTEXTS[type](id, ...args); +} + +var ContextWorker = Class({ + implements: [ WorkerChild ], + + // Calls the context workers context listeners and returns the first result + // that is either a string or a value that evaluates to true. If all of the + // listeners returned false then returns false. If there are no listeners, + // returns true (show the menu item by default). + getMatchedContext: function getCurrentContexts(popupNode) { + let results = this.sandbox.emitSync("context", popupNode); + if (!results.length) + return true; + return results.reduce((val, result) => val || result); + }, + + // Emits a click event in the worker's port. popupNode is the node that was + // context-clicked, and clickedItemData is the data of the item that was + // clicked. + fireClick: function fireClick(popupNode, clickedItemData) { + this.sandbox.emitSync("click", popupNode, clickedItemData); + } +}); + +// Gets the item's content script worker for a window, creating one if necessary +// Once created it will be automatically destroyed when the window unloads. +// If there is not content scripts for the item then null will be returned. +function getItemWorkerForWindow(item, window) { + if (!item.contentScript && !item.contentScriptFile) + return null; + + let id = getInnerId(window); + let worker = item.workerMap.get(id); + + if (worker) + return worker; + + worker = ContextWorker({ + id: item.id, + window, + manager: item.manager, + contentScript: item.contentScript, + contentScriptFile: item.contentScriptFile, + onDetach: function() { + item.workerMap.delete(id); + } + }); + + item.workerMap.set(id, worker); + + return worker; +} + +// A very simple remote proxy for every item. It's job is to provide data for +// the main process to use to determine visibility state and to call into +// content scripts when clicked. +var RemoteItem = Class({ + initialize: function(options, manager) { + this.id = options.id; + this.contexts = options.contexts.map(instantiateContext); + this.contentScript = options.contentScript; + this.contentScriptFile = options.contentScriptFile; + + this.manager = manager; + + this.workerMap = new Map(); + keepAlive.set(this.id, this); + }, + + destroy: function() { + for (let worker of this.workerMap.values()) { + worker.destroy(); + } + keepAlive.delete(this.id); + }, + + activate: function(popupNode, data) { + let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView); + if (!worker) + return; + + for (let context of this.contexts) + popupNode = context.adjustPopupNode(popupNode); + + worker.fireClick(popupNode, data); + }, + + // Fills addonInfo with state data to send through to the main process + getContextState: function(popupNode, addonInfo) { + if (!(self.id in addonInfo)) { + addonInfo[self.id] = { + processID: process.id, + items: {} + }; + } + + let worker = getItemWorkerForWindow(this, popupNode.ownerDocument.defaultView); + let contextStates = {}; + for (let context of this.contexts) + contextStates[context.id] = context.getState(popupNode); + + addonInfo[self.id].items[this.id] = { + // It isn't ideal to create a PageContext for every item but there isn't + // a good shared place to do it. + pageContext: (new CONTEXTS.PageContext()).getState(popupNode), + contextStates, + hasWorker: !!worker, + workerContext: worker ? worker.getMatchedContext(popupNode) : true + } + } +}); +exports.RemoteItem = RemoteItem; + +// Holds remote items for this frame. +var keepAlive = new Map(); + +// Called to create remote proxies for items. If they already exist we destroy +// and recreate. This can happen if the item changes in some way or in odd +// timing cases where the frame script is create around the same time as the +// item is created in the main process +process.port.on('sdk/contextmenu/createitems', (process, items) => { + for (let itemoptions of items) { + let oldItem = keepAlive.get(itemoptions.id); + if (oldItem) { + oldItem.destroy(); + } + + let item = new RemoteItem(itemoptions, this); + } +}); + +process.port.on('sdk/contextmenu/destroyitems', (process, items) => { + for (let id of items) { + let item = keepAlive.get(id); + item.destroy(); + } +}); + +var lastPopupNode = null; + +system.on('content-contextmenu', ({ subject }) => { + let { event: { target: popupNode }, addonInfo } = subject.wrappedJSObject; + lastPopupNode = popupNode; + + for (let item of keepAlive.values()) { + item.getContextState(popupNode, addonInfo); + } +}, true); + +process.port.on('sdk/contextmenu/activateitems', (process, items, data) => { + for (let id of items) { + let item = keepAlive.get(id); + if (!item) + continue; + + item.activate(lastPopupNode, data); + } +}); diff --git a/addon-sdk/source/lib/sdk/content/events.js b/addon-sdk/source/lib/sdk/content/events.js new file mode 100644 index 000000000..c085b6179 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/events.js @@ -0,0 +1,57 @@ +/* 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 { Ci } = require("chrome"); +const { open } = require("../event/dom"); +const { observe } = require("../event/chrome"); +const { filter, merge, map, expand } = require("../event/utils"); +const { windows } = require("../window/utils"); +const { events: windowEvents } = require("sdk/window/events"); + +// Note: Please note that even though pagehide event is included +// it's not observable reliably since it's not always triggered +// when closing tabs. Implementation can be imrpoved once that +// event will be necessary. +var TYPES = ["DOMContentLoaded", "load", "pageshow", "pagehide"]; + +var insert = observe("document-element-inserted"); +var windowCreate = merge([ + observe("content-document-global-created"), + observe("chrome-document-global-created") +]); +var create = map(windowCreate, function({target, data, type}) { + return { target: target.document, type: type, data: data } +}); + +function streamEventsFrom({document}) { + // Map supported event types to a streams of those events on the given + // `window` for the inserted document and than merge these streams into + // single form stream off all window state change events. + let stateChanges = TYPES.map(function(type) { + return open(document, type, { capture: true }); + }); + + // Since load events on document occur for every loded resource + return filter(merge(stateChanges), function({target}) { + return target instanceof Ci.nsIDOMDocument + }) +} +exports.streamEventsFrom = streamEventsFrom; + +var opened = windows(null, { includePrivate: true }); +var state = merge(opened.map(streamEventsFrom)); + + +var futureReady = filter(windowEvents, ({type}) => + type === "DOMContentLoaded"); +var futureWindows = map(futureReady, ({target}) => target); +var futureState = expand(futureWindows, streamEventsFrom); + +exports.events = merge([insert, create, state, futureState]); diff --git a/addon-sdk/source/lib/sdk/content/l10n-html.js b/addon-sdk/source/lib/sdk/content/l10n-html.js new file mode 100644 index 000000000..f324623dc --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/l10n-html.js @@ -0,0 +1,133 @@ +/* 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, Cc, Cu } = require("chrome"); +const core = require("../l10n/core"); +const { loadSheet, removeSheet } = require("../stylesheet/utils"); +const { process, frames } = require("../remote/child"); +var observerService = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); +const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm"); +const addObserver = ShimWaiver.getProperty(observerService, "addObserver"); +const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver"); + +const assetsURI = require('../self').data.url(); + +const hideSheetUri = "data:text/css,:root {visibility: hidden !important;}"; + +function translateElementAttributes(element) { + // Translateable attributes + const attrList = ['title', 'accesskey', 'alt', 'label', 'placeholder']; + const ariaAttrMap = { + 'ariaLabel': 'aria-label', + 'ariaValueText': 'aria-valuetext', + 'ariaMozHint': 'aria-moz-hint' + }; + const attrSeparator = '.'; + + // Try to translate each of the attributes + for (let attribute of attrList) { + const data = core.get(element.dataset.l10nId + attrSeparator + attribute); + if (data) + element.setAttribute(attribute, data); + } + + // Look for the aria attribute translations that match fxOS's aliases + for (let attrAlias in ariaAttrMap) { + const data = core.get(element.dataset.l10nId + attrSeparator + attrAlias); + if (data) + element.setAttribute(ariaAttrMap[attrAlias], data); + } +} + +// Taken from Gaia: +// https://github.com/andreasgal/gaia/blob/04fde2640a7f40314643016a5a6c98bf3755f5fd/webapi.js#L1470 +function translateElement(element) { + element = element || document; + + // check all translatable children (= w/ a `data-l10n-id' attribute) + var children = element.querySelectorAll('*[data-l10n-id]'); + var elementCount = children.length; + for (var i = 0; i < elementCount; i++) { + var child = children[i]; + + // translate the child + var key = child.dataset.l10nId; + var data = core.get(key); + if (data) + child.textContent = data; + + translateElementAttributes(child); + } +} +exports.translateElement = translateElement; + +function onDocumentReady2Translate(event) { + let document = event.target; + document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate, + false); + + translateElement(document); + + try { + // Finally display document when we finished replacing all text content + if (document.defaultView) + removeSheet(document.defaultView, hideSheetUri, 'user'); + } + catch(e) { + console.exception(e); + } +} + +function onContentWindow(document) { + // Accept only HTML documents + if (!(document instanceof Ci.nsIDOMHTMLDocument)) + return; + + // Bug 769483: data:URI documents instanciated with nsIDOMParser + // have a null `location` attribute at this time + if (!document.location) + return; + + // Accept only document from this addon + if (document.location.href.indexOf(assetsURI) !== 0) + return; + + try { + // First hide content of the document in order to have content blinking + // between untranslated and translated states + loadSheet(document.defaultView, hideSheetUri, 'user'); + } + catch(e) { + console.exception(e); + } + // Wait for DOM tree to be built before applying localization + document.addEventListener("DOMContentLoaded", onDocumentReady2Translate, + false); +} + +// Listen to creation of content documents in order to translate them as soon +// as possible in their loading process +const ON_CONTENT = "document-element-inserted"; +let enabled = false; +function enable() { + if (enabled) + return; + addObserver(onContentWindow, ON_CONTENT, false); + enabled = true; +} +process.port.on("sdk/l10n/html/enable", enable); + +function disable() { + if (!enabled) + return; + removeObserver(onContentWindow, ON_CONTENT); + enabled = false; +} +process.port.on("sdk/l10n/html/disable", disable); diff --git a/addon-sdk/source/lib/sdk/content/loader.js b/addon-sdk/source/lib/sdk/content/loader.js new file mode 100644 index 000000000..e4f0dd2aa --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/loader.js @@ -0,0 +1,74 @@ +/* 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 { isValidURI, isLocalURL, URL } = require('../url'); +const { contract } = require('../util/contract'); +const { isString, isNil, instanceOf, isJSONable } = require('../lang/type'); +const { validateOptions, + string, array, object, either, required } = require('../deprecated/api-utils'); + +const isValidScriptFile = (value) => + (isString(value) || instanceOf(value, URL)) && isLocalURL(value); + +// map of property validations +const valid = { + contentURL: { + is: either(string, object), + ok: url => isNil(url) || isLocalURL(url) || isValidURI(url), + msg: 'The `contentURL` option must be a valid URL.' + }, + contentScriptFile: { + is: either(string, object, array), + ok: value => isNil(value) || [].concat(value).every(isValidScriptFile), + msg: 'The `contentScriptFile` option must be a local URL or an array of URLs.' + }, + contentScript: { + is: either(string, array), + ok: value => isNil(value) || [].concat(value).every(isString), + msg: 'The `contentScript` option must be a string or an array of strings.' + }, + contentScriptWhen: { + is: required(string), + map: value => value || 'end', + ok: value => ~['start', 'ready', 'end'].indexOf(value), + msg: 'The `contentScriptWhen` option must be either "start", "ready" or "end".' + }, + contentScriptOptions: { + ok: value => isNil(value) || isJSONable(value), + msg: 'The contentScriptOptions should be a jsonable value.' + } +}; +exports.validationAttributes = valid; + +/** + * Shortcut function to validate property with validation. + * @param {Object|Number|String} suspect + * value to validate + * @param {Object} validation + * validation rule passed to `api-utils` + */ +function validate(suspect, validation) { + return validateOptions( + { $: suspect }, + { $: validation } + ).$; +} + +function Allow(script) { + return { + get script() { + return script; + }, + set script(value) { + script = !!value; + } + }; +} + +exports.contract = contract(valid); diff --git a/addon-sdk/source/lib/sdk/content/mod.js b/addon-sdk/source/lib/sdk/content/mod.js new file mode 100644 index 000000000..81fe9ee42 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/mod.js @@ -0,0 +1,68 @@ +/* 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 { Ci } = require("chrome"); +const { dispatcher } = require("../util/dispatcher"); +const { add, remove, iterator } = require("../lang/weak-set"); + +var getTargetWindow = dispatcher("getTargetWindow"); + +getTargetWindow.define(function (target) { + if (target instanceof Ci.nsIDOMWindow) + return target; + if (target instanceof Ci.nsIDOMDocument) + return target.defaultView || null; + + return null; +}); + +exports.getTargetWindow = getTargetWindow; + +var attachTo = dispatcher("attachTo"); +exports.attachTo = attachTo; + +var detachFrom = dispatcher("detatchFrom"); +exports.detachFrom = detachFrom; + +function attach(modification, target) { + if (!modification) + return; + + let window = getTargetWindow(target); + + attachTo(modification, window); + + // modification are stored per content; `window` reference can still be the + // same even if the content is changed, therefore `document` is used instead. + add(modification, window.document); +} +exports.attach = attach; + +function detach(modification, target) { + if (!modification) + return; + + if (target) { + let window = getTargetWindow(target); + detachFrom(modification, window); + remove(modification, window.document); + } + else { + let documents = iterator(modification); + for (let document of documents) { + let window = document.defaultView; + // The window might have already gone away + if (!window) + continue; + detachFrom(modification, document.defaultView); + remove(modification, document); + } + } +} +exports.detach = detach; diff --git a/addon-sdk/source/lib/sdk/content/page-mod.js b/addon-sdk/source/lib/sdk/content/page-mod.js new file mode 100644 index 000000000..8ff9b1e7b --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/page-mod.js @@ -0,0 +1,236 @@ +/* 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": "stable" +}; + +const { getAttachEventType } = require('../content/utils'); +const { Class } = require('../core/heritage'); +const { Disposable } = require('../core/disposable'); +const { WeakReference } = require('../core/reference'); +const { WorkerChild } = require('./worker-child'); +const { EventTarget } = require('../event/target'); +const { on, emit, once, setListeners } = require('../event/core'); +const { on: domOn, removeListener: domOff } = require('../dom/events'); +const { isRegExp, isUndefined } = require('../lang/type'); +const { merge } = require('../util/object'); +const { isBrowser, getFrames } = require('../window/utils'); +const { getTabs, getURI: getTabURI } = require('../tabs/utils'); +const { ignoreWindow } = require('../private-browsing/utils'); +const { Style } = require("../stylesheet/style"); +const { attach, detach } = require("../content/mod"); +const { has, hasAny } = require("../util/array"); +const { Rules } = require("../util/rules"); +const { List, addListItem, removeListItem } = require('../util/list'); +const { when } = require("../system/unload"); +const { uuid } = require('../util/uuid'); +const { frames, process } = require('../remote/child'); + +const pagemods = new Map(); +const styles = new WeakMap(); +var styleFor = (mod) => styles.get(mod); + +// Helper functions +var modMatchesURI = (mod, uri) => mod.include.matchesAny(uri) && !mod.exclude.matchesAny(uri); + +/** + * PageMod constructor (exported below). + * @constructor + */ +const ChildPageMod = Class({ + implements: [ + EventTarget, + Disposable, + ], + setup: function PageMod(model) { + merge(this, model); + + // Set listeners on {PageMod} itself, not the underlying worker, + // like `onMessage`, as it'll get piped. + setListeners(this, model); + + function deserializeRules(rules) { + for (let rule of rules) { + yield rule.type == "string" ? rule.value + : new RegExp(rule.pattern, rule.flags); + } + } + + let include = [...deserializeRules(this.include)]; + this.include = Rules(); + this.include.add.apply(this.include, include); + + let exclude = [...deserializeRules(this.exclude)]; + this.exclude = Rules(); + this.exclude.add.apply(this.exclude, exclude); + + if (this.contentStyle || this.contentStyleFile) { + styles.set(this, Style({ + uri: this.contentStyleFile, + source: this.contentStyle + })); + } + + pagemods.set(this.id, this); + this.seenDocuments = new WeakMap(); + + // `applyOnExistingDocuments` has to be called after `pagemods.add()` + // otherwise its calls to `onContent` method won't do anything. + if (has(this.attachTo, 'existing')) + applyOnExistingDocuments(this); + }, + + dispose: function() { + let style = styleFor(this); + if (style) + detach(style); + + for (let i in this.include) + this.include.remove(this.include[i]); + + pagemods.delete(this.id); + } +}); + +function onContentWindow({ target: document }) { + // Return if we have no pagemods + if (pagemods.size === 0) + return; + + let window = document.defaultView; + // XML documents don't have windows, and we don't yet support them. + if (!window) + return; + + // Frame event listeners are bound to the frame the event came from by default + let frame = this; + // We apply only on documents in tabs of Firefox + if (!frame.isTab) + return; + + // When the tab is private, only addons with 'private-browsing' flag in + // their package.json can apply content script to private documents + if (ignoreWindow(window)) + return; + + for (let pagemod of pagemods.values()) { + if (modMatchesURI(pagemod, window.location.href)) + onContent(pagemod, window); + } +} +frames.addEventListener("DOMDocElementInserted", onContentWindow, true); + +function applyOnExistingDocuments (mod) { + for (let frame of frames) { + // Fake a newly created document + let window = frame.content; + // 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 = window.location.href; + if (has(mod.attachTo, "top") && modMatchesURI(mod, uri)) + onContent(mod, window); + if (has(mod.attachTo, "frame")) + getFrames(window). + filter(iframe => modMatchesURI(mod, iframe.location.href)). + forEach(frame => onContent(mod, frame)); + } +} + +function createWorker(mod, window) { + let workerId = String(uuid()); + + // Instruct the parent to connect to this worker. Do this first so the parent + // side is connected before the worker attempts to send any messages there + let frame = frames.getFrameForWindow(window.top); + frame.port.emit('sdk/page-mod/worker-create', mod.id, { + id: workerId, + url: window.location.href + }); + + // Create a child worker and notify the parent + let worker = WorkerChild({ + id: workerId, + window: window, + contentScript: mod.contentScript, + contentScriptFile: mod.contentScriptFile, + contentScriptOptions: mod.contentScriptOptions + }); + + once(worker, 'detach', () => worker.destroy()); +} + +function onContent (mod, window) { + let isTopDocument = window.top === window; + // Is a top level document and `top` is not set, ignore + if (isTopDocument && !has(mod.attachTo, "top")) + return; + // Is a frame document and `frame` is not set, ignore + if (!isTopDocument && !has(mod.attachTo, "frame")) + return; + + // ensure we attach only once per document + let seen = mod.seenDocuments; + if (seen.has(window.document)) + return; + seen.set(window.document, true); + + let style = styleFor(mod); + if (style) + attach(style, window); + + // Immediately evaluate content script if the document state is already + // matching contentScriptWhen expectations + if (isMatchingAttachState(mod, window)) { + createWorker(mod, window); + return; + } + + let eventName = getAttachEventType(mod) || 'load'; + domOn(window, eventName, function onReady (e) { + if (e.target.defaultView !== window) + return; + domOff(window, eventName, onReady, true); + createWorker(mod, window); + + // Attaching is asynchronous so if the document is already loaded we will + // miss the pageshow event so send a synthetic one. + if (window.document.readyState == "complete") { + mod.on('attach', worker => { + try { + worker.send('pageshow'); + emit(worker, 'pageshow'); + } + catch (e) { + // This can fail if an earlier attach listener destroyed the worker + } + }); + } + }, true); +} + +function isMatchingAttachState (mod, window) { + let state = window.document.readyState; + return 'start' === mod.contentScriptWhen || + // Is `load` event already dispatched? + 'complete' === state || + // Is DOMContentLoaded already dispatched and waiting for it? + ('ready' === mod.contentScriptWhen && state === 'interactive') +} + +process.port.on('sdk/page-mod/create', (process, model) => { + if (pagemods.has(model.id)) + return; + + new ChildPageMod(model); +}); + +process.port.on('sdk/page-mod/destroy', (process, id) => { + let mod = pagemods.get(id); + if (mod) + mod.destroy(); +}); diff --git a/addon-sdk/source/lib/sdk/content/page-worker.js b/addon-sdk/source/lib/sdk/content/page-worker.js new file mode 100644 index 000000000..e9e741120 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/page-worker.js @@ -0,0 +1,154 @@ +/* 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 { frames } = require("../remote/child"); +const { Class } = require("../core/heritage"); +const { Disposable } = require('../core/disposable'); +const { data } = require("../self"); +const { once } = require("../dom/events"); +const { getAttachEventType } = require("./utils"); +const { Rules } = require('../util/rules'); +const { uuid } = require('../util/uuid'); +const { WorkerChild } = require("./worker-child"); +const { Cc, Ci, Cu } = require("chrome"); +const { observe } = require("../event/chrome"); +const { on } = require("../event/core"); + +const appShell = Cc["@mozilla.org/appshell/appShellService;1"].getService(Ci.nsIAppShellService); + +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); + +const pages = new Map(); + +const DOC_INSERTED = "document-element-inserted"; + +function isValidURL(page, url) { + return !page.rules || page.rules.matchesAny(url); +} + +const ChildPage = Class({ + implements: [ Disposable ], + setup: function(frame, id, options) { + this.id = id; + this.frame = frame; + this.options = options; + + this.webNav = appShell.createWindowlessBrowser(false); + this.docShell.allowJavascript = this.options.allow.script; + + // Accessing the browser's window forces the initial about:blank document to + // be created before we start listening for notifications + this.contentWindow; + + this.webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); + + pages.set(this.id, this); + + this.contentURL = options.contentURL; + + if (options.include) { + this.rules = Rules(); + this.rules.add.apply(this.rules, [].concat(options.include)); + } + }, + + dispose: function() { + pages.delete(this.id); + this.webProgress.removeProgressListener(this); + this.webNav.close(); + this.webNav = null; + }, + + attachWorker: function() { + if (!isValidURL(this, this.contentWindow.location.href)) + return; + + this.options.id = uuid().toString(); + this.options.window = this.contentWindow; + this.frame.port.emit("sdk/frame/connect", this.id, { + id: this.options.id, + url: this.contentWindow.document.documentURIObject.spec + }); + new WorkerChild(this.options); + }, + + get docShell() { + return this.webNav.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + }, + + get webProgress() { + return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + }, + + get contentWindow() { + return this.docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + }, + + get contentURL() { + return this.options.contentURL; + }, + set contentURL(url) { + this.options.contentURL = url; + + url = this.options.contentURL ? data.url(this.options.contentURL) : "about:blank"; + this.webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null); + }, + + onLocationChange: function(progress, request, location, flags) { + // Ignore inner-frame events + if (progress != this.webProgress) + return; + // Ignore events that don't change the document + if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) + return; + + let event = getAttachEventType(this.options); + // Attaching at the start of the load is handled by the + // document-element-inserted listener. + if (event == DOC_INSERTED) + return; + + once(this.contentWindow, event, () => { + this.attachWorker(); + }, false); + }, + + QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"]) +}); + +on(observe(DOC_INSERTED), "data", ({ target }) => { + let page = Array.from(pages.values()).find(p => p.contentWindow.document === target); + if (!page) + return; + + if (getAttachEventType(page.options) == DOC_INSERTED) + page.attachWorker(); +}); + +frames.port.on("sdk/frame/create", (frame, id, options) => { + new ChildPage(frame, id, options); +}); + +frames.port.on("sdk/frame/set", (frame, id, params) => { + let page = pages.get(id); + if (!page) + return; + + if ("allowScript" in params) + page.docShell.allowJavascript = params.allowScript; + if ("contentURL" in params) + page.contentURL = params.contentURL; +}); + +frames.port.on("sdk/frame/destroy", (frame, id) => { + let page = pages.get(id); + if (!page) + return; + + page.destroy(); +}); diff --git a/addon-sdk/source/lib/sdk/content/sandbox.js b/addon-sdk/source/lib/sdk/content/sandbox.js new file mode 100644 index 000000000..096ba5c87 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/sandbox.js @@ -0,0 +1,426 @@ +/* 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 { Class } = require('../core/heritage'); +const { EventTarget } = require('../event/target'); +const { on, off, emit } = require('../event/core'); +const { events } = require('./sandbox/events'); +const { requiresAddonGlobal } = require('./utils'); +const { delay: async } = require('../lang/functional'); +const { Ci, Cu, Cc } = require('chrome'); +const timer = require('../timers'); +const { URL } = require('../url'); +const { sandbox, evaluate, load } = require('../loader/sandbox'); +const { merge } = require('../util/object'); +const { getTabForContentWindowNoShim } = require('../tabs/utils'); +const { getInnerId } = require('../window/utils'); +const { PlainTextConsole } = require('../console/plain-text'); +const { data } = require('../self');const { isChildLoader } = require('../remote/core'); +// WeakMap of sandboxes so we can access private values +const sandboxes = new WeakMap(); + +/* Trick the linker in order to ensure shipping these files in the XPI. + require('./content-worker.js'); + Then, retrieve URL of these files in the XPI: +*/ +var prefix = module.uri.split('sandbox.js')[0]; +const CONTENT_WORKER_URL = prefix + 'content-worker.js'; +const metadata = require('@loader/options').metadata; + +// Fetch additional list of domains to authorize access to for each content +// script. It is stored in manifest `metadata` field which contains +// package.json data. This list is originaly defined by authors in +// `permissions` attribute of their package.json addon file. +const permissions = (metadata && metadata['permissions']) || {}; +const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || []; + +const waiveSecurityMembrane = !!permissions['unsafe-content-script']; + +const nsIScriptSecurityManager = Ci.nsIScriptSecurityManager; +const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + +const JS_VERSION = '1.8'; + +// Tests whether this window is loaded in a tab +function isWindowInTab(window) { + if (isChildLoader) { + let { frames } = require('../remote/child'); + let frame = frames.getFrameForWindow(window.top); + return frame && frame.isTab; + } + else { + // The deprecated sync worker API still does everything in the main process + return getTabForContentWindowNoShim(window); + } +} + +const WorkerSandbox = Class({ + implements: [ EventTarget ], + + /** + * Emit a message to the worker content sandbox + */ + emit: function emit(type, ...args) { + // JSON.stringify is buggy with cross-sandbox values, + // it may return "{}" on functions. Use a replacer to match them correctly. + let replacer = (k, v) => + typeof(v) === "function" + ? (type === "console" ? Function.toString.call(v) : void(0)) + : v; + + // Ensure having an asynchronous behavior + async(() => + emitToContent(this, JSON.stringify([type, ...args], replacer)) + ); + }, + + /** + * Synchronous version of `emit`. + * /!\ Should only be used when it is strictly mandatory /!\ + * Doesn't ensure passing only JSON values. + * Mainly used by context-menu in order to avoid breaking it. + */ + emitSync: function emitSync(...args) { + // because the arguments could be also non JSONable values, + // we need to ensure the array instance is created from + // the content's sandbox + return emitToContent(this, new modelFor(this).sandbox.Array(...args)); + }, + + /** + * Configures sandbox and loads content scripts into it. + * @param {Worker} worker + * content worker + */ + initialize: function WorkerSandbox(worker, window) { + let model = {}; + sandboxes.set(this, model); + model.worker = worker; + // We receive a wrapped window, that may be an xraywrapper if it's content + let proto = window; + + // TODO necessary? + // Ensure that `emit` has always the right `this` + this.emit = this.emit.bind(this); + this.emitSync = this.emitSync.bind(this); + + // Use expanded principal for content-script if the content is a + // regular web content for better isolation. + // (This behavior can be turned off for now with the unsafe-content-script + // flag to give addon developers time for making the necessary changes) + // But prevent it when the Worker isn't used for a content script but for + // injecting `addon` object into a Panel scope, for example. + // That's because: + // 1/ It is useless to use multiple domains as the worker is only used + // to communicate with the addon, + // 2/ By using it it would prevent the document to have access to any JS + // value of the worker. As JS values coming from multiple domain principals + // can't be accessed by 'mono-principals' (principal with only one domain). + // Even if this principal is for a domain that is specified in the multiple + // domain principal. + let principals = window; + let wantGlobalProperties = []; + let isSystemPrincipal = secMan.isSystemPrincipal( + window.document.nodePrincipal); + if (!isSystemPrincipal && !requiresAddonGlobal(worker)) { + if (EXPANDED_PRINCIPALS.length > 0) { + // We have to replace XHR constructor of the content document + // with a custom cross origin one, automagically added by platform code: + delete proto.XMLHttpRequest; + wantGlobalProperties.push('XMLHttpRequest'); + } + if (!waiveSecurityMembrane) + principals = EXPANDED_PRINCIPALS.concat(window); + } + + // Create the sandbox and bind it to window in order for content scripts to + // have access to all standard globals (window, document, ...) + let content = sandbox(principals, { + sandboxPrototype: proto, + wantXrays: !requiresAddonGlobal(worker), + wantGlobalProperties: wantGlobalProperties, + wantExportHelpers: true, + sameZoneAs: window, + metadata: { + SDKContentScript: true, + 'inner-window-id': getInnerId(window) + } + }); + model.sandbox = content; + + // We have to ensure that window.top and window.parent are the exact same + // object than window object, i.e. the sandbox global object. But not + // always, in case of iframes, top and parent are another window object. + let top = window.top === window ? content : content.top; + let parent = window.parent === window ? content : content.parent; + merge(content, { + // We need 'this === window === top' to be true in toplevel scope: + get window() { + return content; + }, + get top() { + return top; + }, + get parent() { + return parent; + } + }); + + // Use the Greasemonkey naming convention to provide access to the + // unwrapped window object so the content script can access document + // JavaScript values. + // NOTE: this functionality is experimental and may change or go away + // at any time! + // + // Note that because waivers aren't propagated between origins, we + // need the unsafeWindow getter to live in the sandbox. + var unsafeWindowGetter = + new content.Function('return window.wrappedJSObject || window;'); + Object.defineProperty(content, 'unsafeWindow', {get: unsafeWindowGetter}); + + // Load trusted code that will inject content script API. + let ContentWorker = load(content, CONTENT_WORKER_URL); + + // prepare a clean `self.options` + let options = 'contentScriptOptions' in worker ? + JSON.stringify(worker.contentScriptOptions) : + undefined; + + // Then call `inject` method and communicate with this script + // by trading two methods that allow to send events to the other side: + // - `onEvent` called by content script + // - `result.emitToContent` called by addon script + let onEvent = Cu.exportFunction(onContentEvent.bind(null, this), ContentWorker); + let chromeAPI = createChromeAPI(ContentWorker); + let result = Cu.waiveXrays(ContentWorker).inject(content, chromeAPI, onEvent, options); + + // Merge `emitToContent` into our private model of the + // WorkerSandbox so we can communicate with content script + model.emitToContent = result; + + let console = new PlainTextConsole(null, getInnerId(window)); + + // Handle messages send by this script: + setListeners(this, console); + + // Inject `addon` global into target document if document is trusted, + // `addon` in document is equivalent to `self` in content script. + if (requiresAddonGlobal(worker)) { + Object.defineProperty(getUnsafeWindow(window), 'addon', { + value: content.self, + configurable: true + } + ); + } + + // Inject our `console` into target document if worker doesn't have a tab + // (e.g Panel, PageWorker). + // `worker.tab` can't be used because bug 804935. + if (!isWindowInTab(window)) { + let win = getUnsafeWindow(window); + + // export our chrome console to content window, as described here: + // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn + let con = Cu.createObjectIn(win); + + let genPropDesc = function genPropDesc(fun) { + return { enumerable: true, configurable: true, writable: true, + value: console[fun] }; + } + + const properties = { + log: genPropDesc('log'), + info: genPropDesc('info'), + warn: genPropDesc('warn'), + error: genPropDesc('error'), + debug: genPropDesc('debug'), + trace: genPropDesc('trace'), + dir: genPropDesc('dir'), + group: genPropDesc('group'), + groupCollapsed: genPropDesc('groupCollapsed'), + groupEnd: genPropDesc('groupEnd'), + time: genPropDesc('time'), + timeEnd: genPropDesc('timeEnd'), + profile: genPropDesc('profile'), + profileEnd: genPropDesc('profileEnd'), + exception: genPropDesc('exception'), + assert: genPropDesc('assert'), + count: genPropDesc('count'), + table: genPropDesc('table'), + clear: genPropDesc('clear'), + dirxml: genPropDesc('dirxml'), + markTimeline: genPropDesc('markTimeline'), + timeline: genPropDesc('timeline'), + timelineEnd: genPropDesc('timelineEnd'), + timeStamp: genPropDesc('timeStamp'), + }; + + Object.defineProperties(con, properties); + Cu.makeObjectPropsNormal(con); + + win.console = con; + }; + + emit(events, "content-script-before-inserted", { + window: window, + worker: worker + }); + + // The order of `contentScriptFile` and `contentScript` evaluation is + // intentional, so programs can load libraries like jQuery from script URLs + // and use them in scripts. + let contentScriptFile = ('contentScriptFile' in worker) + ? worker.contentScriptFile + : null, + contentScript = ('contentScript' in worker) + ? worker.contentScript + : null; + + if (contentScriptFile) + importScripts.apply(null, [this].concat(contentScriptFile)); + + if (contentScript) { + evaluateIn( + this, + Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript + ); + } + }, + destroy: function destroy(reason) { + if (typeof reason != 'string') + reason = ''; + this.emitSync('event', 'detach', reason); + let model = modelFor(this); + model.sandbox = null + model.worker = null; + }, + +}); + +exports.WorkerSandbox = WorkerSandbox; + +/** + * Imports scripts to the sandbox by reading files under urls and + * evaluating its source. If exception occurs during evaluation + * `'error'` event is emitted on the worker. + * This is actually an analog to the `importScript` method in web + * workers but in our case it's not exposed even though content + * scripts may be able to do it synchronously since IO operation + * takes place in the UI process. + */ +function importScripts (workerSandbox, ...urls) { + let { worker, sandbox } = modelFor(workerSandbox); + for (let i in urls) { + let contentScriptFile = data.url(urls[i]); + + try { + let uri = URL(contentScriptFile); + if (uri.scheme === 'resource') + load(sandbox, String(uri)); + else + throw Error('Unsupported `contentScriptFile` url: ' + String(uri)); + } + catch(e) { + emit(worker, 'error', e); + } + } +} + +function setListeners (workerSandbox, console) { + let { worker } = modelFor(workerSandbox); + // console.xxx calls + workerSandbox.on('console', function consoleListener (kind, ...args) { + console[kind].apply(console, args); + }); + + // self.postMessage calls + workerSandbox.on('message', function postMessage(data) { + // destroyed? + if (worker) + emit(worker, 'message', data); + }); + + // self.port.emit calls + workerSandbox.on('event', function portEmit (...eventArgs) { + // If not destroyed, emit event information to worker + // `eventArgs` has the event name as first element, + // and remaining elements are additional arguments to pass + if (worker) + emit.apply(null, [worker.port].concat(eventArgs)); + }); + + // unwrap, recreate and propagate async Errors thrown from content-script + workerSandbox.on('error', function onError({instanceOfError, value}) { + if (worker) { + let error = value; + if (instanceOfError) { + error = new Error(value.message, value.fileName, value.lineNumber); + error.stack = value.stack; + error.name = value.name; + } + emit(worker, 'error', error); + } + }); +} + +/** + * Evaluates code in the sandbox. + * @param {String} code + * JavaScript source to evaluate. + * @param {String} [filename='javascript:' + code] + * Name of the file + */ +function evaluateIn (workerSandbox, code, filename) { + let { worker, sandbox } = modelFor(workerSandbox); + try { + evaluate(sandbox, code, filename || 'javascript:' + code); + } + catch(e) { + emit(worker, 'error', e); + } +} + +/** + * Method called by the worker sandbox when it needs to send a message + */ +function onContentEvent (workerSandbox, args) { + // As `emit`, we ensure having an asynchronous behavior + async(function () { + // We emit event to chrome/addon listeners + emit.apply(null, [workerSandbox].concat(JSON.parse(args))); + }); +} + + +function modelFor (workerSandbox) { + return sandboxes.get(workerSandbox); +} + +function getUnsafeWindow (win) { + return win.wrappedJSObject || win; +} + +function emitToContent (workerSandbox, args) { + return modelFor(workerSandbox).emitToContent(args); +} + +function createChromeAPI (scope) { + return Cu.cloneInto({ + timers: { + setTimeout: timer.setTimeout.bind(timer), + setInterval: timer.setInterval.bind(timer), + clearTimeout: timer.clearTimeout.bind(timer), + clearInterval: timer.clearInterval.bind(timer), + }, + sandbox: { + evaluate: evaluate, + }, + }, scope, {cloneFunctions: true}); +} diff --git a/addon-sdk/source/lib/sdk/content/sandbox/events.js b/addon-sdk/source/lib/sdk/content/sandbox/events.js new file mode 100644 index 000000000..d6f7eb004 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/sandbox/events.js @@ -0,0 +1,12 @@ +/* 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 events = {}; +exports.events = events; diff --git a/addon-sdk/source/lib/sdk/content/tab-events.js b/addon-sdk/source/lib/sdk/content/tab-events.js new file mode 100644 index 000000000..9e244a853 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/tab-events.js @@ -0,0 +1,58 @@ +/* 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 { Ci } = require('chrome'); +const system = require('sdk/system/events'); +const { frames } = require('sdk/remote/child'); +const { WorkerChild } = require('sdk/content/worker-child'); + +// map observer topics to tab event names +const EVENTS = { + 'content-document-global-created': 'create', + 'chrome-document-global-created': 'create', + 'content-document-interactive': 'ready', + 'chrome-document-interactive': 'ready', + 'content-document-loaded': 'load', + 'chrome-document-loaded': 'load', +// 'content-page-shown': 'pageshow', // bug 1024105 +} + +function topicListener({ subject, type }) { + // NOTE detect the window from the subject: + // - on *-global-created the subject is the window + // - in the other cases it is the document object + let window = subject instanceof Ci.nsIDOMWindow ? subject : subject.defaultView; + if (!window){ + return; + } + let frame = frames.getFrameForWindow(window); + if (frame) { + let readyState = frame.content.document.readyState; + frame.port.emit('sdk/tab/event', EVENTS[type], { readyState }); + } +} + +for (let topic in EVENTS) + system.on(topic, topicListener, true); + +// bug 1024105 - content-page-shown notification doesn't pass persisted param +function eventListener({target, type, persisted}) { + let frame = this; + if (target === frame.content.document) { + frame.port.emit('sdk/tab/event', type, persisted); + } +} +frames.addEventListener('pageshow', eventListener, true); + +frames.port.on('sdk/tab/attach', (frame, options) => { + options.window = frame.content; + new WorkerChild(options); +}); + +// Forward the existent frames's readyState. +for (let frame of frames) { + let readyState = frame.content.document.readyState; + frame.port.emit('sdk/tab/event', 'init', { readyState }); +} diff --git a/addon-sdk/source/lib/sdk/content/thumbnail.js b/addon-sdk/source/lib/sdk/content/thumbnail.js new file mode 100644 index 000000000..783615fc6 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/thumbnail.js @@ -0,0 +1,51 @@ +/* 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, Cu } = require('chrome'); +const AppShellService = Cc['@mozilla.org/appshell/appShellService;1']. + getService(Ci.nsIAppShellService); + +const NS = 'http://www.w3.org/1999/xhtml'; +const COLOR = 'rgb(255,255,255)'; + +/** + * Creates canvas element with a thumbnail of the passed window. + * @param {Window} window + * @returns {Element} + */ +function getThumbnailCanvasForWindow(window) { + let aspectRatio = 0.5625; // 16:9 + let thumbnail = AppShellService.hiddenDOMWindow.document + .createElementNS(NS, 'canvas'); + thumbnail.mozOpaque = true; + thumbnail.width = Math.ceil(window.screen.availWidth / 5.75); + thumbnail.height = Math.round(thumbnail.width * aspectRatio); + let ctx = thumbnail.getContext('2d'); + let snippetWidth = window.innerWidth * .6; + let scale = thumbnail.width / snippetWidth; + ctx.scale(scale, scale); + ctx.drawWindow(window, window.scrollX, window.scrollY, snippetWidth, + snippetWidth * aspectRatio, COLOR); + return thumbnail; +} +exports.getThumbnailCanvasForWindow = getThumbnailCanvasForWindow; + +/** + * Creates Base64 encoded data URI of the thumbnail for the passed window. + * @param {Window} window + * @returns {String} + */ +exports.getThumbnailURIForWindow = function getThumbnailURIForWindow(window) { + return getThumbnailCanvasForWindow(window).toDataURL() +}; + +// default 80x45 blank when not available +exports.BLANK = 'data:image/png;base64,' + + 'iVBORw0KGgoAAAANSUhEUgAAAFAAAAAtCAYAAAA5reyyAAAAJElEQVRoge3BAQ'+ + 'EAAACCIP+vbkhAAQAAAAAAAAAAAAAAAADXBjhtAAGQ0AF/AAAAAElFTkSuQmCC'; diff --git a/addon-sdk/source/lib/sdk/content/utils.js b/addon-sdk/source/lib/sdk/content/utils.js new file mode 100644 index 000000000..90995a614 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/utils.js @@ -0,0 +1,105 @@ +/* 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' +}; + +var { merge } = require('../util/object'); +var { data } = require('../self'); +var assetsURI = data.url(); +var isArray = Array.isArray; +var method = require('../../method/core'); +var { uuid } = require('../util/uuid'); + +const isAddonContent = ({ contentURL }) => + contentURL && data.url(contentURL).startsWith(assetsURI); + +exports.isAddonContent = isAddonContent; + +function hasContentScript({ contentScript, contentScriptFile }) { + return (isArray(contentScript) ? contentScript.length > 0 : + !!contentScript) || + (isArray(contentScriptFile) ? contentScriptFile.length > 0 : + !!contentScriptFile); +} +exports.hasContentScript = hasContentScript; + +function requiresAddonGlobal(model) { + return model.injectInDocument || (isAddonContent(model) && !hasContentScript(model)); +} +exports.requiresAddonGlobal = requiresAddonGlobal; + +function getAttachEventType(model) { + if (!model) return null; + let when = model.contentScriptWhen; + return requiresAddonGlobal(model) ? 'document-element-inserted' : + when === 'start' ? 'document-element-inserted' : + when === 'ready' ? 'DOMContentLoaded' : + when === 'end' ? 'load' : + null; +} +exports.getAttachEventType = getAttachEventType; + +var attach = method('worker-attach'); +exports.attach = attach; + +var connect = method('worker-connect'); +exports.connect = connect; + +var detach = method('worker-detach'); +exports.detach = detach; + +var destroy = method('worker-destroy'); +exports.destroy = destroy; + +function WorkerHost (workerFor) { + // Define worker properties that just proxy to underlying worker + return ['postMessage', 'port', 'url', 'tab'].reduce(function(proto, name) { + // Use descriptor properties instead so we can call + // the worker function in the context of the worker so we + // don't have to create new functions with `fn.bind(worker)` + let descriptorProp = { + value: function (...args) { + let worker = workerFor(this); + return worker[name].apply(worker, args); + } + }; + + let accessorProp = { + get: function () { return workerFor(this)[name]; }, + set: function (value) { workerFor(this)[name] = value; } + }; + + Object.defineProperty(proto, name, merge({ + enumerable: true, + configurable: false, + }, isDescriptor(name) ? descriptorProp : accessorProp)); + return proto; + }, {}); + + function isDescriptor (prop) { + return ~['postMessage'].indexOf(prop); + } +} +exports.WorkerHost = WorkerHost; + +function makeChildOptions(options) { + function makeStringArray(arrayOrValue) { + if (!arrayOrValue) + return []; + return [].concat(arrayOrValue).map(String); + } + + return { + id: String(uuid()), + contentScript: makeStringArray(options.contentScript), + contentScriptFile: makeStringArray(options.contentScriptFile), + contentScriptOptions: options.contentScriptOptions ? + JSON.stringify(options.contentScriptOptions) : + null, + } +} +exports.makeChildOptions = makeChildOptions; diff --git a/addon-sdk/source/lib/sdk/content/worker-child.js b/addon-sdk/source/lib/sdk/content/worker-child.js new file mode 100644 index 000000000..dbf65a933 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/worker-child.js @@ -0,0 +1,158 @@ +/* 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('../util/object'); +const { Class } = require('../core/heritage'); +const { emit } = require('../event/core'); +const { EventTarget } = require('../event/target'); +const { getInnerId, getByInnerId } = require('../window/utils'); +const { instanceOf, isObject } = require('../lang/type'); +const system = require('../system/events'); +const { when } = require('../system/unload'); +const { WorkerSandbox } = require('./sandbox'); +const { Ci } = require('chrome'); +const { process, frames } = require('../remote/child'); + +const EVENTS = { + 'chrome-page-shown': 'pageshow', + 'content-page-shown': 'pageshow', + 'chrome-page-hidden': 'pagehide', + 'content-page-hidden': 'pagehide', + 'inner-window-destroyed': 'detach', +} + +// The parent Worker must have been created (or an async message sent to spawn +// its creation) before creating the WorkerChild or messages from the content +// script to the parent will get lost. +const WorkerChild = Class({ + implements: [EventTarget], + + initialize(options) { + merge(this, options); + keepAlive.set(this.id, this); + + this.windowId = getInnerId(this.window); + if (this.contentScriptOptions) + this.contentScriptOptions = JSON.parse(this.contentScriptOptions); + + this.port = EventTarget(); + this.port.on('*', this.send.bind(this, 'event')); + this.on('*', this.send.bind(this)); + + this.observe = this.observe.bind(this); + + for (let topic in EVENTS) + system.on(topic, this.observe); + + this.receive = this.receive.bind(this); + process.port.on('sdk/worker/message', this.receive); + + this.sandbox = WorkerSandbox(this, this.window); + + // If the document has an unexpected readyState, its worker-child instance is initialized + // as frozen until one of the known readyState is reached. + let initialDocumentReadyState = this.window.document.readyState; + this.frozen = [ + "loading", "interactive", "complete" + ].includes(initialDocumentReadyState) ? false : true; + + if (this.frozen) { + console.warn("SDK worker-child started as frozen on unexpected initial document.readyState", { + initialDocumentReadyState, windowLocation: this.window.location.href, + }); + } + + this.frozenMessages = []; + this.on('pageshow', () => { + this.frozen = false; + this.frozenMessages.forEach(args => this.sandbox.emit(...args)); + this.frozenMessages = []; + }); + this.on('pagehide', () => { + this.frozen = true; + }); + }, + + // messages + receive(process, id, args) { + if (id !== this.id) + return; + args = JSON.parse(args); + + if (this.frozen) + this.frozenMessages.push(args); + else + this.sandbox.emit(...args); + + if (args[0] === 'detach') + this.destroy(args[1]); + }, + + send(...args) { + process.port.emit('sdk/worker/event', this.id, JSON.stringify(args, exceptions)); + }, + + // notifications + observe({ type, subject }) { + if (!this.sandbox) + return; + + if (subject.defaultView && getInnerId(subject.defaultView) === this.windowId) { + this.sandbox.emitSync(EVENTS[type]); + emit(this, EVENTS[type]); + } + + if (type === 'inner-window-destroyed' && + subject.QueryInterface(Ci.nsISupportsPRUint64).data === this.windowId) { + this.destroy(); + } + }, + + get frame() { + return frames.getFrameForWindow(this.window.top); + }, + + // detach/destroy: unload and release the sandbox + destroy(reason) { + if (!this.sandbox) + return; + + for (let topic in EVENTS) + system.off(topic, this.observe); + process.port.off('sdk/worker/message', this.receive); + + this.sandbox.destroy(reason); + this.sandbox = null; + keepAlive.delete(this.id); + + this.send('detach'); + } +}) +exports.WorkerChild = WorkerChild; + +// Error instances JSON poorly +function exceptions(key, value) { + if (!isObject(value) || !instanceOf(value, Error)) + return value; + let _errorType = value.constructor.name; + let { message, fileName, lineNumber, stack, name } = value; + return { _errorType, message, fileName, lineNumber, stack, name }; +} + +// workers for windows in this tab +var keepAlive = new Map(); + +process.port.on('sdk/worker/create', (process, options, cpows) => { + options.window = cpows.window; + let worker = new WorkerChild(options); + + let frame = frames.getFrameForWindow(options.window.top); + frame.port.emit('sdk/worker/connect', options.id, options.window.location.href); +}); + +when(reason => { + for (let worker of keepAlive.values()) + worker.destroy(reason); +}); diff --git a/addon-sdk/source/lib/sdk/content/worker.js b/addon-sdk/source/lib/sdk/content/worker.js new file mode 100644 index 000000000..39b940a88 --- /dev/null +++ b/addon-sdk/source/lib/sdk/content/worker.js @@ -0,0 +1,180 @@ +/* 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, merge } = require('../util/object'); +const { Class } = require('../core/heritage'); +const { method } = require('../lang/functional'); +const { getInnerId } = require('../window/utils'); +const { EventTarget } = require('../event/target'); +const { isPrivate } = require('../private-browsing/utils'); +const { getTabForBrowser, getTabForContentWindowNoShim, getBrowserForTab } = require('../tabs/utils'); +const { attach, connect, detach, destroy, makeChildOptions } = require('./utils'); +const { ensure } = require('../system/unload'); +const { on: observe } = require('../system/events'); +const { Ci, Cu } = require('chrome'); +const { modelFor: tabFor } = require('sdk/model/core'); +const { remoteRequire, processes, frames } = require('../remote/parent'); +remoteRequire('sdk/content/worker-child'); + +const workers = new WeakMap(); +var 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."; + +// a handle for communication between content script and addon code +const Worker = Class({ + implements: [EventTarget], + + initialize(options = {}) { + ensure(this, 'detach'); + + let model = { + attached: false, + destroyed: false, + earlyEvents: [], // fired before worker was attached + frozen: true, // document is not yet active + options, + }; + workers.set(this, model); + + this.on('detach', this.detach); + EventTarget.prototype.initialize.call(this, options); + + this.receive = this.receive.bind(this); + + this.port = EventTarget(); + this.port.emit = this.send.bind(this, 'event'); + this.postMessage = this.send.bind(this, 'message'); + + if ('window' in options) { + let window = options.window; + delete options.window; + attach(this, window); + } + }, + + // messages + receive(process, id, args) { + let model = modelFor(this); + if (id !== model.id || !model.attached) + return; + args = JSON.parse(args); + if (model.destroyed && args[0] != 'detach') + return; + + if (args[0] === 'event') + emit(this.port, ...args.slice(1)) + else + emit(this, ...args); + }, + + send(...args) { + let model = modelFor(this); + if (model.destroyed && args[0] !== 'detach') + throw new Error(ERR_DESTROYED); + + if (!model.attached) { + model.earlyEvents.push(args); + return; + } + + processes.port.emit('sdk/worker/message', model.id, JSON.stringify(args)); + }, + + // properties + get url() { + let { url } = modelFor(this); + return url; + }, + + get contentURL() { + return this.url; + }, + + get tab() { + require('sdk/tabs'); + let { frame } = modelFor(this); + if (!frame) + return null; + let rawTab = getTabForBrowser(frame.frameElement); + return rawTab && tabFor(rawTab); + }, + + toString: () => '[object Worker]', + + detach: method(detach), + destroy: method(destroy), +}) +exports.Worker = Worker; + +attach.define(Worker, function(worker, window) { + let model = modelFor(worker); + if (model.attached) + detach(worker); + + let childOptions = makeChildOptions(model.options); + processes.port.emitCPOW('sdk/worker/create', [childOptions], { window }); + + let listener = (frame, id, url) => { + if (id != childOptions.id) + return; + frames.port.off('sdk/worker/connect', listener); + connect(worker, frame, { id, url }); + }; + frames.port.on('sdk/worker/connect', listener); +}); + +connect.define(Worker, function(worker, frame, { id, url }) { + let model = modelFor(worker); + if (model.attached) + detach(worker); + + model.id = id; + model.frame = frame; + model.url = url; + + // Messages from content -> chrome come through the process message manager + // since that lives longer than the frame message manager + processes.port.on('sdk/worker/event', worker.receive); + + model.attached = true; + model.destroyed = false; + model.frozen = false; + + model.earlyEvents.forEach(args => worker.send(...args)); + model.earlyEvents = []; + emit(worker, 'attach'); +}); + +// unload and release the child worker, release window reference +detach.define(Worker, function(worker) { + let model = modelFor(worker); + if (!model.attached) + return; + + processes.port.off('sdk/worker/event', worker.receive); + model.attached = false; + model.destroyed = true; + emit(worker, 'detach'); +}); + +isPrivate.define(Worker, ({ tab }) => isPrivate(tab)); + +// Something in the parent side has destroyed the worker, tell the child to +// detach, the child will respond when it has detached +destroy.define(Worker, function(worker, reason) { + let model = modelFor(worker); + model.destroyed = true; + if (!model.attached) + return; + + worker.send('detach', reason); +}); diff --git a/addon-sdk/source/lib/sdk/context-menu.js b/addon-sdk/source/lib/sdk/context-menu.js new file mode 100644 index 000000000..004c642d4 --- /dev/null +++ b/addon-sdk/source/lib/sdk/context-menu.js @@ -0,0 +1,1188 @@ +/* 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": "stable", + "engines": { + // TODO Fennec support Bug 788334 + "Firefox": "*", + "SeaMonkey": "*" + } +}; + +const { Class, mix } = require("./core/heritage"); +const { addCollectionProperty } = require("./util/collection"); +const { ns } = require("./core/namespace"); +const { validateOptions, getTypeOf } = require("./deprecated/api-utils"); +const { URL, isValidURI } = require("./url"); +const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils"); +const { isBrowser, getInnerId } = require("./window/utils"); +const { MatchPattern } = require("./util/match-pattern"); +const { EventTarget } = require("./event/target"); +const { emit } = require('./event/core'); +const { when } = require('./system/unload'); +const { contract: loaderContract } = require('./content/loader'); +const { omit } = require('./util/object'); +const self = require('./self') +const { remoteRequire, processes } = require('./remote/parent'); +remoteRequire('sdk/content/context-menu'); + +// All user items we add have this class. +const ITEM_CLASS = "addon-context-menu-item"; + +// Items in the top-level context menu also have this class. +const TOPLEVEL_ITEM_CLASS = "addon-context-menu-item-toplevel"; + +// Items in the overflow submenu also have this class. +const OVERFLOW_ITEM_CLASS = "addon-context-menu-item-overflow"; + +// The class of the menu separator that separates standard context menu items +// from our user items. +const SEPARATOR_CLASS = "addon-context-menu-separator"; + +// If more than this number of items are added to the context menu, all items +// overflow into a "Jetpack" submenu. +const OVERFLOW_THRESH_DEFAULT = 10; +const OVERFLOW_THRESH_PREF = + "extensions.addon-sdk.context-menu.overflowThreshold"; + +// The label of the overflow sub-xul:menu. +// +// TODO: Localize these. +const OVERFLOW_MENU_LABEL = "Add-ons"; +const OVERFLOW_MENU_ACCESSKEY = "A"; + +// The class of the overflow sub-xul:menu. +const OVERFLOW_MENU_CLASS = "addon-content-menu-overflow-menu"; + +// The class of the overflow submenu's xul:menupopup. +const OVERFLOW_POPUP_CLASS = "addon-content-menu-overflow-popup"; + +// Holds private properties for API objects +var internal = ns(); + +// A little hacky but this is the last process ID that last opened the context +// menu +var lastContextProcessId = null; + +var uuidModule = require('./util/uuid'); +function uuid() { + return uuidModule.uuid().toString(); +} + +function getScheme(spec) { + try { + return URL(spec).scheme; + } + catch(e) { + return null; + } +} + +var Context = Class({ + initialize: function() { + internal(this).id = uuid(); + }, + + // Returns the node that made this context current + adjustPopupNode: function adjustPopupNode(popupNode) { + return popupNode; + }, + + // Returns whether this context is current for the current node + isCurrent: function isCurrent(state) { + return state; + } +}); + +// Matches when the context-clicked node doesn't have any of +// NON_PAGE_CONTEXT_ELTS in its ancestors +var PageContext = Class({ + extends: Context, + + serialize: function() { + return { + id: internal(this).id, + type: "PageContext", + args: [] + } + } +}); +exports.PageContext = PageContext; + +// Matches when there is an active selection in the window +var SelectionContext = Class({ + extends: Context, + + serialize: function() { + return { + id: internal(this).id, + type: "SelectionContext", + args: [] + } + } +}); +exports.SelectionContext = SelectionContext; + +// Matches when the context-clicked node or any of its ancestors matches the +// selector given +var SelectorContext = Class({ + extends: Context, + + initialize: function initialize(selector) { + Context.prototype.initialize.call(this); + let options = validateOptions({ selector: selector }, { + selector: { + is: ["string"], + msg: "selector must be a string." + } + }); + internal(this).selector = options.selector; + }, + + serialize: function() { + return { + id: internal(this).id, + type: "SelectorContext", + args: [internal(this).selector] + } + } +}); +exports.SelectorContext = SelectorContext; + +// Matches when the page url matches any of the patterns given +var URLContext = Class({ + extends: Context, + + initialize: function initialize(patterns) { + Context.prototype.initialize.call(this); + patterns = Array.isArray(patterns) ? patterns : [patterns]; + + try { + internal(this).patterns = patterns.map(p => new MatchPattern(p)); + } + catch (err) { + throw new Error("Patterns must be a string, regexp or an array of " + + "strings or regexps: " + err); + } + }, + + isCurrent: function isCurrent(url) { + return internal(this).patterns.some(p => p.test(url)); + }, + + serialize: function() { + return { + id: internal(this).id, + type: "URLContext", + args: [] + } + } +}); +exports.URLContext = URLContext; + +// Matches when the user-supplied predicate returns true +var PredicateContext = Class({ + extends: Context, + + initialize: function initialize(predicate) { + Context.prototype.initialize.call(this); + let options = validateOptions({ predicate: predicate }, { + predicate: { + is: ["function"], + msg: "predicate must be a function." + } + }); + internal(this).predicate = options.predicate; + }, + + isCurrent: function isCurrent(state) { + return internal(this).predicate(state); + }, + + serialize: function() { + return { + id: internal(this).id, + type: "PredicateContext", + args: [] + } + } +}); +exports.PredicateContext = PredicateContext; + +function removeItemFromArray(array, item) { + return array.filter(i => i !== item); +} + +// Converts anything that isn't false, null or undefined into a string +function stringOrNull(val) { + return val ? String(val) : val; +} + +// Shared option validation rules for Item, Menu, and Separator +var baseItemRules = { + parentMenu: { + is: ["object", "undefined"], + ok: function (v) { + if (!v) + return true; + return (v instanceof ItemContainer) || (v instanceof Menu); + }, + msg: "parentMenu must be a Menu or not specified." + }, + context: { + is: ["undefined", "object", "array"], + ok: function (v) { + if (!v) + return true; + let arr = Array.isArray(v) ? v : [v]; + return arr.every(o => o instanceof Context); + }, + msg: "The 'context' option must be a Context object or an array of " + + "Context objects." + }, + onMessage: { + is: ["function", "undefined"] + }, + contentScript: loaderContract.rules.contentScript, + contentScriptFile: loaderContract.rules.contentScriptFile +}; + +var labelledItemRules = mix(baseItemRules, { + label: { + map: stringOrNull, + is: ["string"], + ok: v => !!v, + msg: "The item must have a non-empty string label." + }, + accesskey: { + map: stringOrNull, + is: ["string", "undefined", "null"], + ok: (v) => { + if (!v) { + return true; + } + return typeof v == "string" && v.length === 1; + }, + msg: "The item must have a single character accesskey, or no accesskey." + }, + image: { + map: stringOrNull, + is: ["string", "undefined", "null"], + ok: function (url) { + if (!url) + return true; + return isValidURI(url); + }, + msg: "Image URL validation failed" + } +}); + +// Additional validation rules for Item +var itemRules = mix(labelledItemRules, { + data: { + map: stringOrNull, + is: ["string", "undefined", "null"] + } +}); + +// Additional validation rules for Menu +var menuRules = mix(labelledItemRules, { + items: { + is: ["array", "undefined"], + ok: function (v) { + if (!v) + return true; + return v.every(function (item) { + return item instanceof BaseItem; + }); + }, + msg: "items must be an array, and each element in the array must be an " + + "Item, Menu, or Separator." + } +}); + +// Returns true if any contexts match. If there are no contexts then a +// PageContext is tested instead +function hasMatchingContext(contexts, addonInfo) { + for (let context of contexts) { + if (!(internal(context).id in addonInfo.contextStates)) { + console.error("Missing state for context " + internal(context).id + " this is an error in the SDK modules."); + return false; + } + if (!context.isCurrent(addonInfo.contextStates[internal(context).id])) + return false; + } + + return true; +} + +// Tests whether an item should be visible or not based on its contexts and +// content scripts +function isItemVisible(item, addonInfo, usePageWorker) { + if (!item.context.length) { + if (!addonInfo.hasWorker) + return usePageWorker ? addonInfo.pageContext : true; + } + + if (!hasMatchingContext(item.context, addonInfo)) + return false; + + let context = addonInfo.workerContext; + if (typeof(context) === "string" && context != "") + item.label = context; + + return !!context; +} + +// Called when an item is clicked to send out click events to the content +// scripts +function itemActivated(item, clickedNode) { + let items = [internal(item).id]; + let data = item.data; + + while (item.parentMenu) { + item = item.parentMenu; + items.push(internal(item).id); + } + + let process = processes.getById(lastContextProcessId); + if (process) + process.port.emit('sdk/contextmenu/activateitems', items, data); +} + +function serializeItem(item) { + return { + id: internal(item).id, + contexts: item.context.map(c => c.serialize()), + contentScript: item.contentScript, + contentScriptFile: item.contentScriptFile, + }; +} + +// All things that appear in the context menu extend this +var BaseItem = Class({ + initialize: function initialize() { + internal(this).id = uuid(); + + internal(this).contexts = []; + if ("context" in internal(this).options && internal(this).options.context) { + let contexts = internal(this).options.context; + if (Array.isArray(contexts)) { + for (let context of contexts) + internal(this).contexts.push(context); + } + else { + internal(this).contexts.push(contexts); + } + } + + let parentMenu = internal(this).options.parentMenu; + if (!parentMenu) + parentMenu = contentContextMenu; + + parentMenu.addItem(this); + + Object.defineProperty(this, "contentScript", { + enumerable: true, + value: internal(this).options.contentScript + }); + + // Resolve URIs here as tests may have overriden self + let files = internal(this).options.contentScriptFile; + if (files) { + if (!Array.isArray(files)) + files = [files]; + files = files.map(self.data.url); + } + internal(this).options.contentScriptFile = files; + Object.defineProperty(this, "contentScriptFile", { + enumerable: true, + value: internal(this).options.contentScriptFile + }); + + // Notify all frames of this new item + sendItems([serializeItem(this)]); + }, + + destroy: function destroy() { + if (internal(this).destroyed) + return; + + // Tell all existing frames that this item has been destroyed + processes.port.emit("sdk/contextmenu/destroyitems", [internal(this).id]); + + if (this.parentMenu) + this.parentMenu.removeItem(this); + + internal(this).destroyed = true; + }, + + get context() { + let contexts = internal(this).contexts.slice(0); + contexts.add = (context) => { + internal(this).contexts.push(context); + // Notify all frames that this item has changed + sendItems([serializeItem(this)]); + }; + contexts.remove = (context) => { + internal(this).contexts = internal(this).contexts.filter(c => { + return c != context; + }); + // Notify all frames that this item has changed + sendItems([serializeItem(this)]); + }; + return contexts; + }, + + set context(val) { + internal(this).contexts = val.slice(0); + // Notify all frames that this item has changed + sendItems([serializeItem(this)]); + }, + + get parentMenu() { + return internal(this).parentMenu; + }, +}); + +function workerMessageReceived(process, id, args) { + if (internal(this).id != id) + return; + + emit(this, ...JSON.parse(args)); +} + +// All things that have a label on the context menu extend this +var LabelledItem = Class({ + extends: BaseItem, + implements: [ EventTarget ], + + initialize: function initialize(options) { + BaseItem.prototype.initialize.call(this); + EventTarget.prototype.initialize.call(this, options); + + internal(this).messageListener = workerMessageReceived.bind(this); + processes.port.on('sdk/worker/event', internal(this).messageListener); + }, + + destroy: function destroy() { + if (internal(this).destroyed) + return; + + processes.port.off('sdk/worker/event', internal(this).messageListener); + + BaseItem.prototype.destroy.call(this); + }, + + get label() { + return internal(this).options.label; + }, + + set label(val) { + internal(this).options.label = val; + + MenuManager.updateItem(this); + }, + + get accesskey() { + return internal(this).options.accesskey; + }, + + set accesskey(val) { + internal(this).options.accesskey = val; + + MenuManager.updateItem(this); + }, + + get image() { + return internal(this).options.image; + }, + + set image(val) { + internal(this).options.image = val; + + MenuManager.updateItem(this); + }, + + get data() { + return internal(this).options.data; + }, + + set data(val) { + internal(this).options.data = val; + } +}); + +var Item = Class({ + extends: LabelledItem, + + initialize: function initialize(options) { + internal(this).options = validateOptions(options, itemRules); + + LabelledItem.prototype.initialize.call(this, options); + }, + + toString: function toString() { + return "[object Item \"" + this.label + "\"]"; + }, + + get data() { + return internal(this).options.data; + }, + + set data(val) { + internal(this).options.data = val; + + MenuManager.updateItem(this); + }, +}); +exports.Item = Item; + +var ItemContainer = Class({ + initialize: function initialize() { + internal(this).children = []; + }, + + destroy: function destroy() { + // Destroys the entire hierarchy + for (let item of internal(this).children) + item.destroy(); + }, + + addItem: function addItem(item) { + let oldParent = item.parentMenu; + + // Don't just call removeItem here as that would remove the corresponding + // UI element which is more costly than just moving it to the right place + if (oldParent) + internal(oldParent).children = removeItemFromArray(internal(oldParent).children, item); + + let after = null; + let children = internal(this).children; + if (children.length > 0) + after = children[children.length - 1]; + + children.push(item); + internal(item).parentMenu = this; + + // If there was an old parent then we just have to move the item, otherwise + // it needs to be created + if (oldParent) + MenuManager.moveItem(item, after); + else + MenuManager.createItem(item, after); + }, + + removeItem: function removeItem(item) { + // If the item isn't a child of this menu then ignore this call + if (item.parentMenu !== this) + return; + + MenuManager.removeItem(item); + + internal(this).children = removeItemFromArray(internal(this).children, item); + internal(item).parentMenu = null; + }, + + get items() { + return internal(this).children.slice(0); + }, + + set items(val) { + // Validate the arguments before making any changes + if (!Array.isArray(val)) + throw new Error(menuOptionRules.items.msg); + + for (let item of val) { + if (!(item instanceof BaseItem)) + throw new Error(menuOptionRules.items.msg); + } + + // Remove the old items and add the new ones + for (let item of internal(this).children) + this.removeItem(item); + + for (let item of val) + this.addItem(item); + }, +}); + +var Menu = Class({ + extends: LabelledItem, + implements: [ItemContainer], + + initialize: function initialize(options) { + internal(this).options = validateOptions(options, menuRules); + + LabelledItem.prototype.initialize.call(this, options); + ItemContainer.prototype.initialize.call(this); + + if (internal(this).options.items) { + for (let item of internal(this).options.items) + this.addItem(item); + } + }, + + destroy: function destroy() { + ItemContainer.prototype.destroy.call(this); + LabelledItem.prototype.destroy.call(this); + }, + + toString: function toString() { + return "[object Menu \"" + this.label + "\"]"; + }, +}); +exports.Menu = Menu; + +var Separator = Class({ + extends: BaseItem, + + initialize: function initialize(options) { + internal(this).options = validateOptions(options, baseItemRules); + + BaseItem.prototype.initialize.call(this); + }, + + toString: function toString() { + return "[object Separator]"; + } +}); +exports.Separator = Separator; + +// Holds items for the content area context menu +var contentContextMenu = ItemContainer(); +exports.contentContextMenu = contentContextMenu; + +function getContainerItems(container) { + let items = []; + for (let item of internal(container).children) { + items.push(serializeItem(item)); + if (item instanceof Menu) + items = items.concat(getContainerItems(item)); + } + return items; +} + +// Notify all frames of these new or changed items +function sendItems(items) { + processes.port.emit("sdk/contextmenu/createitems", items); +} + +// Called when a new process is created and needs to get the current list of items +function remoteItemRequest(process) { + let items = getContainerItems(contentContextMenu); + if (items.length == 0) + return; + + process.port.emit("sdk/contextmenu/createitems", items); +} +processes.forEvery(remoteItemRequest); + +when(function() { + contentContextMenu.destroy(); +}); + +// App specific UI code lives here, it should handle populating the context +// menu and passing clicks etc. through to the items. + +function countVisibleItems(nodes) { + return Array.reduce(nodes, function(sum, node) { + return node.hidden ? sum : sum + 1; + }, 0); +} + +var MenuWrapper = Class({ + initialize: function initialize(winWrapper, items, contextMenu) { + this.winWrapper = winWrapper; + this.window = winWrapper.window; + this.items = items; + this.contextMenu = contextMenu; + this.populated = false; + this.menuMap = new Map(); + + // updateItemVisibilities will run first, updateOverflowState will run after + // all other instances of this module have run updateItemVisibilities + this._updateItemVisibilities = this.updateItemVisibilities.bind(this); + this.contextMenu.addEventListener("popupshowing", this._updateItemVisibilities, true); + this._updateOverflowState = this.updateOverflowState.bind(this); + this.contextMenu.addEventListener("popupshowing", this._updateOverflowState, false); + }, + + destroy: function destroy() { + this.contextMenu.removeEventListener("popupshowing", this._updateOverflowState, false); + this.contextMenu.removeEventListener("popupshowing", this._updateItemVisibilities, true); + + if (!this.populated) + return; + + // If we're getting unloaded at runtime then we must remove all the + // generated XUL nodes + let oldParent = null; + for (let item of internal(this.items).children) { + let xulNode = this.getXULNodeForItem(item); + oldParent = xulNode.parentNode; + oldParent.removeChild(xulNode); + } + + if (oldParent) + this.onXULRemoved(oldParent); + }, + + get separator() { + return this.contextMenu.querySelector("." + SEPARATOR_CLASS); + }, + + get overflowMenu() { + return this.contextMenu.querySelector("." + OVERFLOW_MENU_CLASS); + }, + + get overflowPopup() { + return this.contextMenu.querySelector("." + OVERFLOW_POPUP_CLASS); + }, + + get topLevelItems() { + return this.contextMenu.querySelectorAll("." + TOPLEVEL_ITEM_CLASS); + }, + + get overflowItems() { + return this.contextMenu.querySelectorAll("." + OVERFLOW_ITEM_CLASS); + }, + + getXULNodeForItem: function getXULNodeForItem(item) { + return this.menuMap.get(item); + }, + + // Recurses through the item hierarchy creating XUL nodes for everything + populate: function populate(menu) { + for (let i = 0; i < internal(menu).children.length; i++) { + let item = internal(menu).children[i]; + let after = i === 0 ? null : internal(menu).children[i - 1]; + this.createItem(item, after); + + if (item instanceof Menu) + this.populate(item); + } + }, + + // Recurses through the menu setting the visibility of items. Returns true + // if any of the items in this menu were visible + setVisibility: function setVisibility(menu, addonInfo, usePageWorker) { + let anyVisible = false; + + for (let item of internal(menu).children) { + let visible = isItemVisible(item, addonInfo[internal(item).id], usePageWorker); + + // Recurse through Menus, if none of the sub-items were visible then the + // menu is hidden too. + if (visible && (item instanceof Menu)) + visible = this.setVisibility(item, addonInfo, false); + + let xulNode = this.getXULNodeForItem(item); + xulNode.hidden = !visible; + + anyVisible = anyVisible || visible; + } + + return anyVisible; + }, + + // Works out where to insert a XUL node for an item in a browser window + insertIntoXUL: function insertIntoXUL(item, node, after) { + let menupopup = null; + let before = null; + + let menu = item.parentMenu; + if (menu === this.items) { + // Insert into the overflow popup if it exists, otherwise the normal + // context menu + menupopup = this.overflowPopup; + if (!menupopup) + menupopup = this.contextMenu; + } + else { + let xulNode = this.getXULNodeForItem(menu); + menupopup = xulNode.firstChild; + } + + if (after) { + let afterNode = this.getXULNodeForItem(after); + before = afterNode.nextSibling; + } + else if (menupopup === this.contextMenu) { + let topLevel = this.topLevelItems; + if (topLevel.length > 0) + before = topLevel[topLevel.length - 1].nextSibling; + else + before = this.separator.nextSibling; + } + + menupopup.insertBefore(node, before); + }, + + // Sets the right class for XUL nodes + updateXULClass: function updateXULClass(xulNode) { + if (xulNode.parentNode == this.contextMenu) + xulNode.classList.add(TOPLEVEL_ITEM_CLASS); + else + xulNode.classList.remove(TOPLEVEL_ITEM_CLASS); + + if (xulNode.parentNode == this.overflowPopup) + xulNode.classList.add(OVERFLOW_ITEM_CLASS); + else + xulNode.classList.remove(OVERFLOW_ITEM_CLASS); + }, + + // Creates a XUL node for an item + createItem: function createItem(item, after) { + if (!this.populated) + return; + + // Create the separator if it doesn't already exist + if (!this.separator) { + let separator = this.window.document.createElement("menuseparator"); + separator.setAttribute("class", SEPARATOR_CLASS); + + // Insert before the separator created by the old context-menu if it + // exists to avoid bug 832401 + let oldSeparator = this.window.document.getElementById("jetpack-context-menu-separator"); + if (oldSeparator && oldSeparator.parentNode != this.contextMenu) + oldSeparator = null; + this.contextMenu.insertBefore(separator, oldSeparator); + } + + let type = "menuitem"; + if (item instanceof Menu) + type = "menu"; + else if (item instanceof Separator) + type = "menuseparator"; + + let xulNode = this.window.document.createElement(type); + xulNode.setAttribute("class", ITEM_CLASS); + if (item instanceof LabelledItem) { + xulNode.setAttribute("label", item.label); + if (item.accesskey) + xulNode.setAttribute("accesskey", item.accesskey); + if (item.image) { + xulNode.setAttribute("image", item.image); + if (item instanceof Menu) + xulNode.classList.add("menu-iconic"); + else + xulNode.classList.add("menuitem-iconic"); + } + if (item.data) + xulNode.setAttribute("value", item.data); + + let self = this; + xulNode.addEventListener("command", function(event) { + // Only care about clicks directly on this item + if (event.target !== xulNode) + return; + + itemActivated(item, xulNode); + }, false); + } + + this.insertIntoXUL(item, xulNode, after); + this.updateXULClass(xulNode); + xulNode.data = item.data; + + if (item instanceof Menu) { + let menupopup = this.window.document.createElement("menupopup"); + xulNode.appendChild(menupopup); + } + + this.menuMap.set(item, xulNode); + }, + + // Updates the XUL node for an item in this window + updateItem: function updateItem(item) { + if (!this.populated) + return; + + let xulNode = this.getXULNodeForItem(item); + + // TODO figure out why this requires setAttribute + xulNode.setAttribute("label", item.label); + xulNode.setAttribute("accesskey", item.accesskey || ""); + + if (item.image) { + xulNode.setAttribute("image", item.image); + if (item instanceof Menu) + xulNode.classList.add("menu-iconic"); + else + xulNode.classList.add("menuitem-iconic"); + } + else { + xulNode.removeAttribute("image"); + xulNode.classList.remove("menu-iconic"); + xulNode.classList.remove("menuitem-iconic"); + } + + if (item.data) + xulNode.setAttribute("value", item.data); + else + xulNode.removeAttribute("value"); + }, + + // Moves the XUL node for an item in this window to its new place in the + // hierarchy + moveItem: function moveItem(item, after) { + if (!this.populated) + return; + + let xulNode = this.getXULNodeForItem(item); + let oldParent = xulNode.parentNode; + + this.insertIntoXUL(item, xulNode, after); + this.updateXULClass(xulNode); + this.onXULRemoved(oldParent); + }, + + // Removes the XUL nodes for an item in every window we've ever populated. + removeItem: function removeItem(item) { + if (!this.populated) + return; + + let xulItem = this.getXULNodeForItem(item); + + let oldParent = xulItem.parentNode; + + oldParent.removeChild(xulItem); + this.menuMap.delete(item); + + this.onXULRemoved(oldParent); + }, + + // Called when any XUL nodes have been removed from a menupopup. This handles + // making sure the separator and overflow are correct + onXULRemoved: function onXULRemoved(parent) { + if (parent == this.contextMenu) { + let toplevel = this.topLevelItems; + + // If there are no more items then remove the separator + if (toplevel.length == 0) { + let separator = this.separator; + if (separator) + separator.parentNode.removeChild(separator); + } + } + else if (parent == this.overflowPopup) { + // If there are no more items then remove the overflow menu and separator + if (parent.childNodes.length == 0) { + let separator = this.separator; + separator.parentNode.removeChild(separator); + this.contextMenu.removeChild(parent.parentNode); + } + } + }, + + // Recurses through all the items owned by this module and sets their hidden + // state + updateItemVisibilities: function updateItemVisibilities(event) { + try { + if (event.type != "popupshowing") + return; + if (event.target != this.contextMenu) + return; + + if (internal(this.items).children.length == 0) + return; + + if (!this.populated) { + this.populated = true; + this.populate(this.items); + } + + let mainWindow = event.target.ownerDocument.defaultView; + this.contextMenuContentData = mainWindow.gContextMenuContentData + if (!(self.id in this.contextMenuContentData.addonInfo)) { + console.warn("No context menu state data was provided."); + return; + } + let addonInfo = this.contextMenuContentData.addonInfo[self.id]; + lastContextProcessId = addonInfo.processID; + this.setVisibility(this.items, addonInfo.items, true); + } + catch (e) { + console.exception(e); + } + }, + + // Counts the number of visible items across all modules and makes sure they + // are in the right place between the top level context menu and the overflow + // menu + updateOverflowState: function updateOverflowState(event) { + try { + if (event.type != "popupshowing") + return; + if (event.target != this.contextMenu) + return; + + // The main items will be in either the top level context menu or the + // overflow menu at this point. Count the visible ones and if they are in + // the wrong place move them + let toplevel = this.topLevelItems; + let overflow = this.overflowItems; + let visibleCount = countVisibleItems(toplevel) + + countVisibleItems(overflow); + + if (visibleCount == 0) { + let separator = this.separator; + if (separator) + separator.hidden = true; + let overflowMenu = this.overflowMenu; + if (overflowMenu) + overflowMenu.hidden = true; + } + else if (visibleCount > MenuManager.overflowThreshold) { + this.separator.hidden = false; + let overflowPopup = this.overflowPopup; + if (overflowPopup) + overflowPopup.parentNode.hidden = false; + + if (toplevel.length > 0) { + // The overflow menu shouldn't exist here but let's play it safe + if (!overflowPopup) { + let overflowMenu = this.window.document.createElement("menu"); + overflowMenu.setAttribute("class", OVERFLOW_MENU_CLASS); + overflowMenu.setAttribute("label", OVERFLOW_MENU_LABEL); + overflowMenu.setAttribute("accesskey", OVERFLOW_MENU_ACCESSKEY); + this.contextMenu.insertBefore(overflowMenu, this.separator.nextSibling); + + overflowPopup = this.window.document.createElement("menupopup"); + overflowPopup.setAttribute("class", OVERFLOW_POPUP_CLASS); + overflowMenu.appendChild(overflowPopup); + } + + for (let xulNode of toplevel) { + overflowPopup.appendChild(xulNode); + this.updateXULClass(xulNode); + } + } + } + else { + this.separator.hidden = false; + + if (overflow.length > 0) { + // Move all the overflow nodes out of the overflow menu and position + // them immediately before it + for (let xulNode of overflow) { + this.contextMenu.insertBefore(xulNode, xulNode.parentNode.parentNode); + this.updateXULClass(xulNode); + } + this.contextMenu.removeChild(this.overflowMenu); + } + } + } + catch (e) { + console.exception(e); + } + } +}); + +// This wraps every window that we've seen +var WindowWrapper = Class({ + initialize: function initialize(window) { + this.window = window; + this.menus = [ + new MenuWrapper(this, contentContextMenu, window.document.getElementById("contentAreaContextMenu")), + ]; + }, + + destroy: function destroy() { + for (let menuWrapper of this.menus) + menuWrapper.destroy(); + }, + + getMenuWrapperForItem: function getMenuWrapperForItem(item) { + let root = item.parentMenu; + while (root.parentMenu) + root = root.parentMenu; + + for (let wrapper of this.menus) { + if (wrapper.items === root) + return wrapper; + } + + return null; + } +}); + +var MenuManager = { + windowMap: new Map(), + + get overflowThreshold() { + let prefs = require("./preferences/service"); + return prefs.get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); + }, + + // When a new window is added start watching it for context menu shows + onTrack: function onTrack(window) { + if (!isBrowser(window)) + return; + + // Generally shouldn't happen, but just in case + if (this.windowMap.has(window)) { + console.warn("Already seen this window"); + return; + } + + let winWrapper = WindowWrapper(window); + this.windowMap.set(window, winWrapper); + }, + + onUntrack: function onUntrack(window) { + if (!isBrowser(window)) + return; + + let winWrapper = this.windowMap.get(window); + // This shouldn't happen but protect against it anyway + if (!winWrapper) + return; + winWrapper.destroy(); + + this.windowMap.delete(window); + }, + + // Creates a XUL node for an item in every window we've already populated + createItem: function createItem(item, after) { + for (let [window, winWrapper] of this.windowMap) { + let menuWrapper = winWrapper.getMenuWrapperForItem(item); + if (menuWrapper) + menuWrapper.createItem(item, after); + } + }, + + // Updates the XUL node for an item in every window we've already populated + updateItem: function updateItem(item) { + for (let [window, winWrapper] of this.windowMap) { + let menuWrapper = winWrapper.getMenuWrapperForItem(item); + if (menuWrapper) + menuWrapper.updateItem(item); + } + }, + + // Moves the XUL node for an item in every window we've ever populated to its + // new place in the hierarchy + moveItem: function moveItem(item, after) { + for (let [window, winWrapper] of this.windowMap) { + let menuWrapper = winWrapper.getMenuWrapperForItem(item); + if (menuWrapper) + menuWrapper.moveItem(item, after); + } + }, + + // Removes the XUL nodes for an item in every window we've ever populated. + removeItem: function removeItem(item) { + for (let [window, winWrapper] of this.windowMap) { + let menuWrapper = winWrapper.getMenuWrapperForItem(item); + if (menuWrapper) + menuWrapper.removeItem(item); + } + } +}; + +WindowTracker(MenuManager); 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 000000000..fc5aea500 --- /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 000000000..c64cddfe8 --- /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 000000000..5078f8f29 --- /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 000000000..45ad804e9 --- /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 new file mode 100644 index 000000000..19f7eaa9f --- /dev/null +++ b/addon-sdk/source/lib/sdk/core/disposable.js @@ -0,0 +1,186 @@ +/* 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 { Class } = require("./heritage"); +const { Observer, subscribe, unsubscribe, observe } = require("./observer"); +const { isWeak } = require("./reference"); +const SDKWeakSet = require("../lang/weak-set"); + +const method = require("../../method/core"); + +const unloadSubject = require('@loader/unload'); +const addonUnloadTopic = "sdk:loader:destroy"; + +const uninstall = method("disposable/uninstall"); +exports.uninstall = uninstall; + +const shutdown = method("disposable/shutdown"); +exports.shutdown = shutdown; + +const disable = method("disposable/disable"); +exports.disable = disable; + +const upgrade = method("disposable/upgrade"); +exports.upgrade = upgrade; + +const downgrade = method("disposable/downgrade"); +exports.downgrade = downgrade; + +const unload = method("disposable/unload"); +exports.unload = unload; + +const dispose = method("disposable/dispose"); +exports.dispose = dispose; +dispose.define(Object, object => object.dispose()); + +const setup = method("disposable/setup"); +exports.setup = setup; +setup.define(Object, (object, ...args) => object.setup(...args)); + +// DisposablesUnloadObserver is the class which subscribe the +// Observer Service to be notified when the add-on loader is +// unloading to be able to dispose all the existent disposables. +const DisposablesUnloadObserver = Class({ + implements: [Observer], + initialize: function(...args) { + // Set of the non-weak disposables registered to be disposed. + this.disposables = new Set(); + // Target of the weak disposables registered to be disposed + // (and tracked on this target using the SDK weak-set module). + this.weakDisposables = {}; + }, + subscribe(disposable) { + if (isWeak(disposable)) { + SDKWeakSet.add(this.weakDisposables, disposable); + } else { + this.disposables.add(disposable); + } + }, + unsubscribe(disposable) { + if (isWeak(disposable)) { + SDKWeakSet.remove(this.weakDisposables, disposable); + } else { + this.disposables.delete(disposable); + } + }, + tryUnloadDisposable(disposable) { + try { + if (disposable) { + unload(disposable); + } + } catch(e) { + console.error("Error unloading a", + isWeak(disposable) ? "weak disposable" : "disposable", + disposable, e); + } + }, + unloadAll() { + // Remove all the subscribed disposables. + for (let disposable of this.disposables) { + this.tryUnloadDisposable(disposable); + } + + this.disposables.clear(); + + // Remove all the subscribed weak disposables. + for (let disposable of SDKWeakSet.iterator(this.weakDisposables)) { + this.tryUnloadDisposable(disposable); + } + + SDKWeakSet.clear(this.weakDisposables); + } +}); +const disposablesUnloadObserver = new DisposablesUnloadObserver(); + +// The DisposablesUnloadObserver instance is the only object which subscribes +// the Observer Service directly, it observes add-on unload notifications in +// order to trigger `unload` on all its subscribed disposables. +observe.define(DisposablesUnloadObserver, (obj, subject, topic, data) => { + const isUnloadTopic = topic === addonUnloadTopic; + const isUnloadSubject = subject.wrappedJSObject === unloadSubject; + if (isUnloadTopic && isUnloadSubject) { + unsubscribe(disposablesUnloadObserver, addonUnloadTopic); + disposablesUnloadObserver.unloadAll(); + } +}); + +subscribe(disposablesUnloadObserver, addonUnloadTopic, false); + +// Set's up disposable instance. +const setupDisposable = disposable => { + disposablesUnloadObserver.subscribe(disposable); +}; +exports.setupDisposable = setupDisposable; + +// Tears down disposable instance. +const disposeDisposable = disposable => { + disposablesUnloadObserver.unsubscribe(disposable); +}; +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. +const Disposable = Class({ + initialize: function(...args) { + // First setup instance before initializing it's disposal. If instance + // fails to initialize then there is no instance to be disposed at the + // unload. + setup(this, ...args); + setupDisposable(this); + }, + destroy: function(reason) { + // Destroying disposable removes unload handler so that attempt to dispose + // won't be made at unload & delegates to dispose. + disposeDisposable(this); + unload(this, reason); + }, + setup: function() { + // Implement your initialize logic here. + }, + dispose: function() { + // Implement your cleanup logic here. + } +}); +exports.Disposable = Disposable; + +const unloaders = { + destroy: dispose, + uninstall: uninstall, + shutdown: shutdown, + disable: disable, + upgrade: upgrade, + downgrade: downgrade +}; + +const unloaded = new WeakMap(); +unload.define(Disposable, (disposable, reason) => { + if (!unloaded.get(disposable)) { + unloaded.set(disposable, true); + // Pick an unload handler associated with an unload + // reason (falling back to destroy if not found) and + // delegate unloading to it. + const unload = unloaders[reason] || unloaders.destroy; + unload(disposable); + } +}); + +// If add-on is disabled manually, it's being upgraded, downgraded +// or uninstalled `dispose` is invoked to undo any changes that +// has being done by it in this session. +disable.define(Disposable, dispose); +downgrade.define(Disposable, dispose); +upgrade.define(Disposable, dispose); +uninstall.define(Disposable, dispose); + +// If application is shut down no dispose is invoked as undo-ing +// changes made by instance is likely to just waste of resources & +// 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/core/heritage.js b/addon-sdk/source/lib/sdk/core/heritage.js new file mode 100644 index 000000000..fc87ba1f5 --- /dev/null +++ b/addon-sdk/source/lib/sdk/core/heritage.js @@ -0,0 +1,184 @@ +/* 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" +}; + +var getPrototypeOf = Object.getPrototypeOf; +var getNames = x => [...Object.getOwnPropertyNames(x), + ...Object.getOwnPropertySymbols(x)]; +var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +var create = Object.create; +var freeze = Object.freeze; +var unbind = Function.call.bind(Function.bind, Function.call); + +// This shortcut makes sure that we do perform desired operations, even if +// associated methods have being overridden on the used object. +var owns = unbind(Object.prototype.hasOwnProperty); +var apply = unbind(Function.prototype.apply); +var slice = Array.slice || unbind(Array.prototype.slice); +var reduce = Array.reduce || unbind(Array.prototype.reduce); +var map = Array.map || unbind(Array.prototype.map); +var concat = Array.concat || unbind(Array.prototype.concat); + +// Utility function to get own properties descriptor map. +function getOwnPropertyDescriptors(object) { + return reduce(getNames(object), function(descriptor, name) { + descriptor[name] = getOwnPropertyDescriptor(object, name); + return descriptor; + }, {}); +} + +function isDataProperty(property) { + var value = property.value; + var type = typeof(property.value); + return "value" in property && + (type !== "object" || value === null) && + type !== "function"; +} + +function getDataProperties(object) { + var properties = getOwnPropertyDescriptors(object); + return getNames(properties).reduce(function(result, name) { + var property = properties[name]; + if (isDataProperty(property)) { + result[name] = { + value: property.value, + writable: true, + configurable: true, + enumerable: false + }; + } + return result; + }, {}) +} + +/** + * Takes `source` object as an argument and returns identical object + * with the difference that all own properties will be non-enumerable + */ +function obscure(source) { + var descriptor = reduce(getNames(source), function(descriptor, name) { + var property = getOwnPropertyDescriptor(source, name); + property.enumerable = false; + descriptor[name] = property; + return descriptor; + }, {}); + return create(getPrototypeOf(source), descriptor); +} +exports.obscure = obscure; + +/** + * Takes arbitrary number of source objects and returns fresh one, that + * inherits from the same prototype as a first argument and implements all + * own properties of all argument objects. If two or more argument objects + * have own properties with the same name, the property is overridden, with + * precedence from right to left, implying, that properties of the object on + * the left are overridden by a same named property of the object on the right. + */ +var mix = function(source) { + var descriptor = reduce(slice(arguments), function(descriptor, source) { + return reduce(getNames(source), function(descriptor, name) { + descriptor[name] = getOwnPropertyDescriptor(source, name); + return descriptor; + }, descriptor); + }, {}); + + return create(getPrototypeOf(source), descriptor); +}; +exports.mix = mix; + +/** + * Returns a frozen object with that inherits from the given `prototype` and + * implements all own properties of the given `properties` object. + */ +function extend(prototype, properties) { + return create(prototype, getOwnPropertyDescriptors(properties)); +} +exports.extend = extend; + +/** + * Returns a constructor function with a proper `prototype` setup. Returned + * constructor's `prototype` inherits from a given `options.extends` or + * `Class.prototype` if omitted and implements all the properties of the + * given `option`. If `options.implemens` array is passed, it's elements + * will be mixed into prototype as well. Also, `options.extends` can be + * a function or a prototype. If function than it's prototype is used as + * an ancestor of the prototype, if it's an object that it's used directly. + * Also `options.implements` may contain functions or objects, in case of + * functions their prototypes are used for mixing. + */ +var Class = new function() { + function prototypeOf(input) { + return typeof(input) === 'function' ? input.prototype : input; + } + var none = freeze([]); + + return function Class(options) { + // Create descriptor with normalized `options.extends` and + // `options.implements`. + var descriptor = { + // Normalize extends property of `options.extends` to a prototype object + // in case it's constructor. If property is missing that fallback to + // `Type.prototype`. + extends: owns(options, 'extends') ? + prototypeOf(options.extends) : Class.prototype, + // Normalize `options.implements` to make sure that it's array of + // prototype objects instead of constructor functions. + implements: owns(options, 'implements') ? + freeze(map(options.implements, prototypeOf)) : none + }; + + // Create array of property descriptors who's properties will be defined + // on the resulting prototype. Note: Using reflection `concat` instead of + // method as it may be overridden. + var descriptors = concat(descriptor.implements, options, descriptor, { + constructor: constructor + }); + + // Note: we use reflection `apply` in the constructor instead of method + // call since later may be overridden. + function constructor() { + var instance = create(prototype, attributes); + if (initialize) apply(initialize, instance, arguments); + return instance; + } + // Create `prototype` that inherits from given ancestor passed as + // `options.extends`, falling back to `Type.prototype`, implementing all + // properties of given `options.implements` and `options` itself. + var prototype = extend(descriptor.extends, mix.apply(mix, descriptors)); + var initialize = prototype.initialize; + + // Combine ancestor attributes with prototype's attributes so that + // ancestors attributes also become initializeable. + var attributes = mix(descriptor.extends.constructor.attributes || {}, + getDataProperties(prototype)); + + constructor.attributes = attributes; + Object.defineProperty(constructor, 'prototype', { + configurable: false, + writable: false, + value: prototype + }); + return constructor; + }; +} +Class.prototype = extend(null, obscure({ + constructor: function constructor() { + this.initialize.apply(this, arguments); + return this; + }, + initialize: function initialize() { + // Do your initialization logic here + }, + // Copy useful properties from `Object.prototype`. + toString: Object.prototype.toString, + toLocaleString: Object.prototype.toLocaleString, + toSource: Object.prototype.toSource, + valueOf: Object.prototype.valueOf, + isPrototypeOf: Object.prototype.isPrototypeOf +})); +exports.Class = freeze(Class); diff --git a/addon-sdk/source/lib/sdk/core/namespace.js b/addon-sdk/source/lib/sdk/core/namespace.js new file mode 100644 index 000000000..3ceb73b72 --- /dev/null +++ b/addon-sdk/source/lib/sdk/core/namespace.js @@ -0,0 +1,43 @@ +/* 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 create = Object.create; +const prototypeOf = Object.getPrototypeOf; + +/** + * Returns a new namespace, function that may can be used to access an + * namespaced object of the argument argument. Namespaced object are associated + * with owner objects via weak references. Namespaced objects inherit from the + * owners ancestor namespaced object. If owner's ancestor is `null` then + * namespaced object inherits from given `prototype`. Namespaces can be used + * to define internal APIs that can be shared via enclosing `namespace` + * function. + * @examples + * const internals = ns(); + * internals(object).secret = secret; + */ +function ns() { + const map = new WeakMap(); + return function namespace(target) { + if (!target) // If `target` is not an object return `target` itself. + return target; + // If target has no namespaced object yet, create one that inherits from + // the target prototype's namespaced object. + if (!map.has(target)) + map.set(target, create(namespace(prototypeOf(target) || null))); + + return map.get(target); + }; +}; + +// `Namespace` is a e4x function in the scope, so we export the function also as +// `ns` as alias to avoid clashing. +exports.ns = ns; +exports.Namespace = ns; diff --git a/addon-sdk/source/lib/sdk/core/observer.js b/addon-sdk/source/lib/sdk/core/observer.js new file mode 100644 index 000000000..7e11bf8f9 --- /dev/null +++ b/addon-sdk/source/lib/sdk/core/observer.js @@ -0,0 +1,89 @@ +/* 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 { Cc, Ci, Cr, Cu } = require("chrome"); +const { Class } = require("./heritage"); +const { isWeak } = require("./reference"); +const method = require("../../method/core"); + +const observerService = Cc['@mozilla.org/observer-service;1']. + getService(Ci.nsIObserverService); + +const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm"); +const addObserver = ShimWaiver.getProperty(observerService, "addObserver"); +const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver"); + +// This is a method that will be invoked when notification observer +// subscribed to occurs. +const observe = method("observer/observe"); +exports.observe = observe; + +// Method to subscribe to the observer notification. +const subscribe = method("observe/subscribe"); +exports.subscribe = subscribe; + + +// Method to unsubscribe from the observer notifications. +const unsubscribe = method("observer/unsubscribe"); +exports.unsubscribe = unsubscribe; + + +// This is wrapper class that takes a `delegate` and produces +// instance of `nsIObserver` which will delegate to a given +// object when observer notification occurs. +const ObserverDelegee = Class({ + initialize: function(delegate) { + this.delegate = delegate; + }, + QueryInterface: function(iid) { + if (!iid.equals(Ci.nsIObserver) && + !iid.equals(Ci.nsISupportsWeakReference) && + !iid.equals(Ci.nsISupports)) + throw Cr.NS_ERROR_NO_INTERFACE; + + return this; + }, + observe: function(subject, topic, data) { + observe(this.delegate, subject, topic, data); + } +}); + + +// Class that can be either mixed in or inherited from in +// order to subscribe / unsubscribe for observer notifications. +const Observer = Class({}); +exports.Observer = Observer; + +// Weak maps that associates instance of `ObserverDelegee` with +// an actual observer. It ensures that `ObserverDelegee` instance +// won't be GC-ed until given `observer` is. +const subscribers = new WeakMap(); + +// Implementation of `subscribe` for `Observer` type just registers +// observer for an observer service. If `isWeak(observer)` is `true` +// observer service won't hold strong reference to a given `observer`. +subscribe.define(Observer, (observer, topic) => { + if (!subscribers.has(observer)) { + const delegee = new ObserverDelegee(observer); + subscribers.set(observer, delegee); + addObserver(delegee, topic, isWeak(observer)); + } +}); + +// Unsubscribes `observer` from observer notifications for the +// given `topic`. +unsubscribe.define(Observer, (observer, topic) => { + const delegee = subscribers.get(observer); + if (delegee) { + subscribers.delete(observer); + removeObserver(delegee, topic); + } +}); diff --git a/addon-sdk/source/lib/sdk/core/promise.js b/addon-sdk/source/lib/sdk/core/promise.js new file mode 100644 index 000000000..f4bd7b0f5 --- /dev/null +++ b/addon-sdk/source/lib/sdk/core/promise.js @@ -0,0 +1,118 @@ +/* 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'; + +/* + * Uses `Promise.jsm` as a core implementation, with additional sugar + * from previous implementation, with inspiration from `Q` and `when` + * + * https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm + * https://github.com/cujojs/when + * https://github.com/kriskowal/q + */ +const PROMISE_URI = 'resource://gre/modules/Promise.jsm'; + +getEnvironment.call(this, function ({ require, exports, module, Cu }) { + +const Promise = Cu.import(PROMISE_URI, {}).Promise; +const { Debugging, defer, resolve, all, reject, race } = Promise; + +module.metadata = { + 'stability': 'unstable' +}; + +var promised = (function() { + // Note: Define shortcuts and utility functions here in order to avoid + // slower property accesses and unnecessary closure creations on each + // call of this popular function. + + var call = Function.call; + var concat = Array.prototype.concat; + + // Utility function that does following: + // execute([ f, self, args...]) => f.apply(self, args) + function execute (args) { + return call.apply(call, args); + } + + // Utility function that takes promise of `a` array and maybe promise `b` + // as arguments and returns promise for `a.concat(b)`. + function promisedConcat(promises, unknown) { + return promises.then(function (values) { + return resolve(unknown) + .then(value => values.concat([value])); + }); + } + + return function promised(f, prototype) { + /** + Returns a wrapped `f`, which when called returns a promise that resolves to + `f(...)` passing all the given arguments to it, which by the way may be + promises. Optionally second `prototype` argument may be provided to be used + a prototype for a returned promise. + + ## Example + + var promise = promised(Array)(1, promise(2), promise(3)) + promise.then(console.log) // => [ 1, 2, 3 ] + **/ + + return function promised(...args) { + // create array of [ f, this, args... ] + return [f, this, ...args]. + // reduce it via `promisedConcat` to get promised array of fulfillments + reduce(promisedConcat, resolve([], prototype)). + // finally map that to promise of `f.apply(this, args...)` + then(execute); + }; + }; +})(); + +exports.promised = promised; +exports.all = all; +exports.defer = defer; +exports.resolve = resolve; +exports.reject = reject; +exports.race = race; +exports.Promise = Promise; +exports.Debugging = Debugging; +}); + +function getEnvironment (callback) { + let Cu, _exports, _module, _require; + + // CommonJS / SDK + if (typeof(require) === 'function') { + Cu = require('chrome').Cu; + _exports = exports; + _module = module; + _require = require; + } + // JSM + else if (String(this).indexOf('BackstagePass') >= 0) { + Cu = this['Components'].utils; + _exports = this.Promise = {}; + _module = { uri: __URI__, id: 'promise/core' }; + _require = uri => { + let imports = {}; + Cu.import(uri, imports); + return imports; + }; + this.EXPORTED_SYMBOLS = ['Promise']; + // mozIJSSubScriptLoader.loadSubscript + } else if (~String(this).indexOf('Sandbox')) { + Cu = this['Components'].utils; + _exports = this; + _module = { id: 'promise/core' }; + _require = uri => {}; + } + + callback({ + Cu: Cu, + exports: _exports, + module: _module, + require: _require + }); +} + diff --git a/addon-sdk/source/lib/sdk/core/reference.js b/addon-sdk/source/lib/sdk/core/reference.js new file mode 100644 index 000000000..04549cd0f --- /dev/null +++ b/addon-sdk/source/lib/sdk/core/reference.js @@ -0,0 +1,29 @@ +/* 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 method = require("../../method/core"); +const { Class } = require("./heritage"); + +// Object that inherit or mix WeakRefence inn will register +// weak observes for system notifications. +const WeakReference = Class({}); +exports.WeakReference = WeakReference; + + +// If `isWeak(object)` is `true` observer installed +// for such `object` will be weak, meaning that it will +// be GC-ed if nothing else but observer is observing it. +// By default everything except `WeakReference` will return +// `false`. +const isWeak = method("reference/weak?"); +exports.isWeak = isWeak; + +isWeak.define(Object, _ => false); +isWeak.define(WeakReference, _ => true); diff --git a/addon-sdk/source/lib/sdk/deprecated/api-utils.js b/addon-sdk/source/lib/sdk/deprecated/api-utils.js new file mode 100644 index 000000000..856fc50cb --- /dev/null +++ b/addon-sdk/source/lib/sdk/deprecated/api-utils.js @@ -0,0 +1,197 @@ +/* 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": "deprecated" +}; + +const { merge } = require("../util/object"); +const { union } = require("../util/array"); +const { isNil, isRegExp } = require("../lang/type"); + +// The possible return values of getTypeOf. +const VALID_TYPES = [ + "array", + "boolean", + "function", + "null", + "number", + "object", + "string", + "undefined", + "regexp" +]; + +const { isArray } = Array; + +/** + * Returns a validated options dictionary given some requirements. If any of + * the requirements are not met, an exception is thrown. + * + * @param options + * An object, the options dictionary to validate. It's not modified. + * If it's null or otherwise falsey, an empty object is assumed. + * @param requirements + * An object whose keys are the expected keys in options. Any key in + * options that is not present in requirements is ignored. Each value + * in requirements is itself an object describing the requirements of + * its key. There are four optional keys in this object: + * map: A function that's passed the value of the key in options. + * map's return value is taken as the key's value in the final + * validated options, is, and ok. If map throws an exception + * it's caught and discarded, and the key's value is its value in + * options. + * is: An array containing any number of the typeof type names. If + * the key's value is none of these types, it fails validation. + * Arrays, null and regexps are identified by the special type names + * "array", "null", "regexp"; "object" will not match either. No type + * coercion is done. + * ok: A function that's passed the key's value. If it returns + * false, the value fails validation. + * msg: If the key's value fails validation, an exception is thrown. + * This string will be used as its message. If undefined, a + * generic message is used, unless is is defined, in which case + * the message will state that the value needs to be one of the + * given types. + * @return An object whose keys are those keys in requirements that are also in + * options and whose values are the corresponding return values of map + * or the corresponding values in options. Note that any keys not + * shared by both requirements and options are not in the returned + * object. + */ +exports.validateOptions = function validateOptions(options, requirements) { + options = options || {}; + let validatedOptions = {}; + + for (let key in requirements) { + let isOptional = false; + let mapThrew = false; + let req = requirements[key]; + let [optsVal, keyInOpts] = (key in options) ? + [options[key], true] : + [undefined, false]; + if (req.map) { + try { + optsVal = req.map(optsVal); + } + catch (err) { + if (err instanceof RequirementError) + throw err; + + mapThrew = true; + } + } + if (req.is) { + let types = req.is; + + if (!isArray(types) && isArray(types.is)) + types = types.is; + + if (isArray(types)) { + isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v)); + + // Sanity check the caller's type names. + types.forEach(function (typ) { + if (VALID_TYPES.indexOf(typ) < 0) { + let msg = 'Internal error: invalid requirement type "' + typ + '".'; + throw new Error(msg); + } + }); + if (types.indexOf(getTypeOf(optsVal)) < 0) + throw new RequirementError(key, req); + } + } + + if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal))) + throw new RequirementError(key, req); + + if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined)) + validatedOptions[key] = optsVal; + } + + return validatedOptions; +}; + +exports.addIterator = function addIterator(obj, keysValsGenerator) { + obj.__iterator__ = function(keysOnly, keysVals) { + let keysValsIterator = keysValsGenerator.call(this); + + // "for (.. in ..)" gets only keys, "for each (.. in ..)" gets values, + // and "for (.. in Iterator(..))" gets [key, value] pairs. + let index = keysOnly ? 0 : 1; + while (true) + yield keysVals ? keysValsIterator.next() : keysValsIterator.next()[index]; + }; +}; + +// Similar to typeof, except arrays, null and regexps are identified by "array" and +// "null" and "regexp", not "object". +var getTypeOf = exports.getTypeOf = function getTypeOf(val) { + let typ = typeof(val); + if (typ === "object") { + if (!val) + return "null"; + if (isArray(val)) + return "array"; + if (isRegExp(val)) + return "regexp"; + } + return typ; +} + +function RequirementError(key, requirement) { + Error.call(this); + + this.name = "RequirementError"; + + let msg = requirement.msg; + if (!msg) { + msg = 'The option "' + key + '" '; + msg += requirement.is ? + "must be one of the following types: " + requirement.is.join(", ") : + "is invalid."; + } + + this.message = msg; +} +RequirementError.prototype = Object.create(Error.prototype); + +var string = { is: ['string', 'undefined', 'null'] }; +exports.string = string; + +var number = { is: ['number', 'undefined', 'null'] }; +exports.number = number; + +var boolean = { is: ['boolean', 'undefined', 'null'] }; +exports.boolean = boolean; + +var object = { is: ['object', 'undefined', 'null'] }; +exports.object = object; + +var array = { is: ['array', 'undefined', 'null'] }; +exports.array = array; + +var isTruthyType = type => !(type === 'undefined' || type === 'null'); +var findTypes = v => { while (!isArray(v) && v.is) v = v.is; return v }; + +function required(req) { + let types = (findTypes(req) || VALID_TYPES).filter(isTruthyType); + + return merge({}, req, {is: types}); +} +exports.required = required; + +function optional(req) { + req = merge({is: []}, req); + req.is = findTypes(req).filter(isTruthyType).concat('undefined', 'null'); + + return req; +} +exports.optional = optional; + +function either(...types) { + return union.apply(null, types.map(findTypes)); +} +exports.either = either; diff --git a/addon-sdk/source/lib/sdk/deprecated/events/assembler.js b/addon-sdk/source/lib/sdk/deprecated/events/assembler.js new file mode 100644 index 000000000..bb297c24f --- /dev/null +++ b/addon-sdk/source/lib/sdk/deprecated/events/assembler.js @@ -0,0 +1,54 @@ +/* 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 } = require("../../core/heritage"); +const { removeListener, on } = require("../../dom/events"); + +/** + * Event targets + * can be added / removed by calling `observe / ignore` methods. Composer should + * provide array of event types it wishes to handle as property + * `supportedEventsTypes` and function for handling all those events as + * `handleEvent` property. + */ +exports.DOMEventAssembler = Class({ + /** + * Function that is supposed to handle all the supported events (that are + * present in the `supportedEventsTypes`) from all the observed + * `eventTargets`. + * @param {Event} event + * Event being dispatched. + */ + handleEvent() { + throw new TypeError("Instance of DOMEventAssembler must implement `handleEvent` method"); + }, + /** + * Array of supported event names. + * @type {String[]} + */ + 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`. + * @param {EventTarget} eventTarget + */ + observe: function observe(eventTarget) { + this.supportedEventsTypes.forEach(function(eventType) { + on(eventTarget, eventType, this); + }, this); + }, + /** + * Removes `eventTarget` from the list of observed `eventTarget`s. Listeners + * for all supported events will be unregistered from the given `eventTarget`. + * @param {EventTarget} eventTarget + */ + ignore: function ignore(eventTarget) { + this.supportedEventsTypes.forEach(function(eventType) { + removeListener(eventTarget, eventType, this); + }, this); + } +}); 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 000000000..71cadac36 --- /dev/null +++ b/addon-sdk/source/lib/sdk/deprecated/sync-worker.js @@ -0,0 +1,288 @@ +/* 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 { isPrivate } = require('../private-browsing/utils'); + +// A weak map of workers to hold private attributes that +// should not be exposed +const workers = new WeakMap(); + +var 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 + * worker. + * + * `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; + }, + + // 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-finder.js b/addon-sdk/source/lib/sdk/deprecated/unit-test-finder.js new file mode 100644 index 000000000..e38629f45 --- /dev/null +++ b/addon-sdk/source/lib/sdk/deprecated/unit-test-finder.js @@ -0,0 +1,199 @@ +/* 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": "deprecated" +}; + +const file = require("../io/file"); +const { Loader } = require("../test/loader"); + +const { isNative } = require('@loader/options'); + +const cuddlefish = isNative ? require("toolkit/loader") : require("../loader/cuddlefish"); + +const { defer, resolve } = require("../core/promise"); +const { getAddon } = require("../addon/installer"); +const { id } = require("sdk/self"); +const { newURI } = require('sdk/url/utils'); +const { getZipReader } = require("../zip/utils"); + +const { Cc, Ci, Cu } = require("chrome"); +const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); +var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + +const CFX_TEST_REGEX = /(([^\/]+\/)(?:lib\/)?)?(tests?\/test-[^\.\/]+)\.js$/; +const JPM_TEST_REGEX = /^()(tests?\/test-[^\.\/]+)\.js$/; + +const { mapcat, map, filter, fromEnumerator } = require("sdk/util/sequence"); + +const toFile = x => x.QueryInterface(Ci.nsIFile); +const isTestFile = ({leafName}) => leafName.substr(0, 5) == "test-" && leafName.substr(-3, 3) == ".js"; +const getFileURI = x => ios.newFileURI(x).spec; + +const getDirectoryEntries = file => map(toFile, fromEnumerator(_ => file.directoryEntries)); +const getTestFiles = directory => filter(isTestFile, getDirectoryEntries(directory)); +const getTestURIs = directory => map(getFileURI, getTestFiles(directory)); + +const isDirectory = x => x.isDirectory(); +const getTestEntries = directory => mapcat(entry => + /^tests?$/.test(entry.leafName) ? getTestURIs(entry) : getTestEntries(entry), + filter(isDirectory, getDirectoryEntries(directory))); + +const removeDups = (array) => array.reduce((result, value) => { + if (value != result[result.length - 1]) { + result.push(value); + } + return result; +}, []); + +const getSuites = function getSuites({ id, filter }) { + const TEST_REGEX = isNative ? JPM_TEST_REGEX : CFX_TEST_REGEX; + + return getAddon(id).then(addon => { + let fileURI = addon.getResourceURI("tests/"); + let isPacked = fileURI.scheme == "jar"; + let xpiURI = addon.getResourceURI(); + let file = xpiURI.QueryInterface(Ci.nsIFileURL).file; + let suites = []; + let addEntry = (entry) => { + if (filter(entry) && TEST_REGEX.test(entry)) { + let suite = (isNative ? "./" : "") + (RegExp.$2 || "") + RegExp.$3; + suites.push(suite); + } + } + + if (isPacked) { + return getZipReader(file).then(zip => { + let entries = zip.findEntries(null); + while (entries.hasMore()) { + let entry = entries.getNext(); + addEntry(entry); + } + zip.close(); + + // sort and remove dups + suites = removeDups(suites.sort()); + return suites; + }) + } + else { + let tests = [...getTestEntries(file)]; + let rootURI = addon.getResourceURI("/"); + tests.forEach((entry) => { + addEntry(entry.replace(rootURI.spec, "")); + }); + } + + // sort and remove dups + suites = removeDups(suites.sort()); + return suites; + }); +} +exports.getSuites = getSuites; + +const makeFilters = function makeFilters(options) { + options = options || {}; + + // A filter string is {fileNameRegex}[:{testNameRegex}] - ie, a colon + // optionally separates a regex for the test fileName from a regex for the + // testName. + if (options.filter) { + let colonPos = options.filter.indexOf(':'); + let filterFileRegex, filterNameRegex; + + if (colonPos === -1) { + filterFileRegex = new RegExp(options.filter); + filterNameRegex = { test: () => true } + } + else { + filterFileRegex = new RegExp(options.filter.substr(0, colonPos)); + filterNameRegex = new RegExp(options.filter.substr(colonPos + 1)); + } + + return { + fileFilter: (name) => filterFileRegex.test(name), + testFilter: (name) => filterNameRegex.test(name) + } + } + + return { + fileFilter: () => true, + testFilter: () => true + }; +} +exports.makeFilters = makeFilters; + +var loader = Loader(module); +const NOT_TESTS = ['setup', 'teardown']; + +var TestFinder = exports.TestFinder = function TestFinder(options) { + this.filter = options.filter; + this.testInProcess = options.testInProcess === false ? false : true; + this.testOutOfProcess = options.testOutOfProcess === true ? true : false; +}; + +TestFinder.prototype = { + findTests: function findTests() { + let { fileFilter, testFilter } = makeFilters({ filter: this.filter }); + + return getSuites({ id: id, filter: fileFilter }).then(suites => { + let testsRemaining = []; + + let getNextTest = () => { + if (testsRemaining.length) { + return testsRemaining.shift(); + } + + if (!suites.length) { + return null; + } + + let suite = suites.shift(); + + // Load each test file as a main module in its own loader instance + // `suite` is defined by cuddlefish/manifest.py:ManifestBuilder.build + let suiteModule; + + try { + suiteModule = cuddlefish.main(loader, suite); + } + catch (e) { + if (/Unsupported Application/i.test(e.message)) { + // If `Unsupported Application` error thrown during test, + // skip the test suite + suiteModule = { + 'test suite skipped': assert => assert.pass(e.message) + }; + } + else { + console.exception(e); + throw e; + } + } + + if (this.testInProcess) { + for (let name of Object.keys(suiteModule).sort()) { + if (NOT_TESTS.indexOf(name) === -1 && testFilter(name)) { + testsRemaining.push({ + setup: suiteModule.setup, + teardown: suiteModule.teardown, + testFunction: suiteModule[name], + name: suite + "." + name + }); + } + } + } + + return getNextTest(); + }; + + return { + getNext: () => resolve(getNextTest()) + }; + }); + } +}; diff --git a/addon-sdk/source/lib/sdk/deprecated/unit-test.js b/addon-sdk/source/lib/sdk/deprecated/unit-test.js new file mode 100644 index 000000000..32bba8f6b --- /dev/null +++ b/addon-sdk/source/lib/sdk/deprecated/unit-test.js @@ -0,0 +1,584 @@ +/* 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": "deprecated" +}; + +const timer = require("../timers"); +const cfxArgs = require("../test/options"); +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"); +const { cleanUI } = require("../test/utils"); + +const findAndRunTests = function findAndRunTests(options) { + var TestFinder = require("./unit-test-finder").TestFinder; + var finder = new TestFinder({ + filter: options.filter, + testInProcess: options.testInProcess, + testOutOfProcess: options.testOutOfProcess + }); + var runner = new TestRunner({fs: options.fs}); + finder.findTests().then(tests => { + runner.startMany({ + tests: tests, + stopOnError: options.stopOnError, + onDone: options.onDone + }); + }); +}; +exports.findAndRunTests = findAndRunTests; + +var runnerWindows = new WeakMap(); +var runnerTabs = new WeakMap(); + +const TestRunner = function TestRunner(options) { + options = options || {}; + + // 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; + this.passed = 0; + this.failed = 0; + this.testRunSummary = []; + this.expectFailNesting = 0; + this.done = TestRunner.prototype.done.bind(this); +}; + +TestRunner.prototype = { + toString: function toString() { + return "[object TestRunner]"; + }, + + DEFAULT_PAUSE_TIMEOUT: (cfxArgs.parseable ? 300000 : 15000), //Five minutes (5*60*1000ms) + PAUSE_DELAY: 500, + + _logTestFailed: function _logTestFailed(why) { + if (!(why in this.test.errors)) + this.test.errors[why] = 0; + this.test.errors[why]++; + }, + + _uncaughtErrorObserver: function({message, date, fileName, stack, lineNumber}) { + this.fail("There was an uncaught Promise rejection: " + message + " @ " + + fileName + ":" + lineNumber + "\n" + stack); + }, + + pass: function pass(message) { + if(!this.expectFailure) { + if ("testMessage" in this.console) + this.console.testMessage(true, true, this.test.name, message); + else + this.console.info("pass:", message); + this.passed++; + this.test.passed++; + this.test.last = message; + } + else { + this.expectFailure = false; + this._logTestFailed("failure"); + if ("testMessage" in this.console) { + this.console.testMessage(true, false, this.test.name, message); + } + else { + this.console.error("fail:", 'Failure Expected: ' + message) + this.console.trace(); + } + this.failed++; + this.test.failed++; + } + }, + + fail: function fail(message) { + if(!this.expectFailure) { + this._logTestFailed("failure"); + if ("testMessage" in this.console) { + this.console.testMessage(false, false, this.test.name, message); + } + else { + this.console.error("fail:", message) + this.console.trace(); + } + this.failed++; + this.test.failed++; + } + else { + this.expectFailure = false; + if ("testMessage" in this.console) + this.console.testMessage(false, true, this.test.name, message); + else + this.console.info("pass:", message); + this.passed++; + this.test.passed++; + this.test.last = message; + } + }, + + expectFail: function(callback) { + this.expectFailure = true; + callback(); + this.expectFailure = false; + }, + + exception: function exception(e) { + this._logTestFailed("exception"); + if (cfxArgs.parseable) + this.console.print("TEST-UNEXPECTED-FAIL | " + this.test.name + " | " + e + "\n"); + this.console.exception(e); + this.failed++; + this.test.failed++; + }, + + assertMatches: function assertMatches(string, regexp, message) { + if (regexp.test(string)) { + if (!message) + message = uneval(string) + " matches " + uneval(regexp); + this.pass(message); + } else { + var no = uneval(string) + " doesn't match " + uneval(regexp); + if (!message) + message = no; + else + message = message + " (" + no + ")"; + this.fail(message); + } + }, + + assertRaises: function assertRaises(func, predicate, message) { + try { + func(); + if (message) + this.fail(message + " (no exception thrown)"); + else + this.fail("function failed to throw exception"); + } catch (e) { + var errorMessage; + if (typeof(e) == "string") + errorMessage = e; + else + errorMessage = e.message; + if (typeof(predicate) == "string") + this.assertEqual(errorMessage, predicate, message); + else + this.assertMatches(errorMessage, predicate, message); + } + }, + + assert: function assert(a, message) { + if (!a) { + if (!message) + message = "assertion failed, value is " + a; + this.fail(message); + } else + this.pass(message || "assertion successful"); + }, + + assertNotEqual: function assertNotEqual(a, b, message) { + if (a != b) { + if (!message) + message = "a != b != " + uneval(a); + this.pass(message); + } else { + var equality = uneval(a) + " == " + uneval(b); + if (!message) + message = equality; + else + message += " (" + equality + ")"; + this.fail(message); + } + }, + + assertEqual: function assertEqual(a, b, message) { + if (a == b) { + if (!message) + message = "a == b == " + uneval(a); + this.pass(message); + } else { + var inequality = uneval(a) + " != " + uneval(b); + if (!message) + message = inequality; + else + message += " (" + inequality + ")"; + this.fail(message); + } + }, + + assertNotStrictEqual: function assertNotStrictEqual(a, b, message) { + if (a !== b) { + if (!message) + message = "a !== b !== " + uneval(a); + this.pass(message); + } else { + var equality = uneval(a) + " === " + uneval(b); + if (!message) + message = equality; + else + message += " (" + equality + ")"; + this.fail(message); + } + }, + + assertStrictEqual: function assertStrictEqual(a, b, message) { + if (a === b) { + if (!message) + message = "a === b === " + uneval(a); + this.pass(message); + } else { + var inequality = uneval(a) + " !== " + uneval(b); + if (!message) + message = inequality; + else + message += " (" + inequality + ")"; + this.fail(message); + } + }, + + assertFunction: function assertFunction(a, message) { + this.assertStrictEqual('function', typeof a, message); + }, + + assertUndefined: function(a, message) { + this.assertStrictEqual('undefined', typeof a, message); + }, + + assertNotUndefined: function(a, message) { + this.assertNotStrictEqual('undefined', typeof a, message); + }, + + assertNull: function(a, message) { + this.assertStrictEqual(null, a, message); + }, + + assertNotNull: function(a, message) { + this.assertNotStrictEqual(null, a, message); + }, + + assertObject: function(a, message) { + this.assertStrictEqual('[object Object]', Object.prototype.toString.apply(a), message); + }, + + assertString: function(a, message) { + this.assertStrictEqual('[object String]', Object.prototype.toString.apply(a), message); + }, + + assertArray: function(a, message) { + this.assertStrictEqual('[object Array]', Object.prototype.toString.apply(a), message); + }, + + assertNumber: function(a, message) { + this.assertStrictEqual('[object Number]', Object.prototype.toString.apply(a), message); + }, + + done: function done() { + if (this.isDone) { + return resolve(); + } + + this.isDone = true; + this.pass("This test is done."); + + if (this.test.teardown) { + this.test.teardown(this); + } + + if (this.waitTimeout !== null) { + timer.clearTimeout(this.waitTimeout); + this.waitTimeout = null; + } + + // Do not leave any callback set when calling to `waitUntil` + this.waitUntilCallback = null; + if (this.test.passed == 0 && this.test.failed == 0) { + this._logTestFailed("empty test"); + + if ("testMessage" in this.console) { + this.console.testMessage(false, false, this.test.name, "Empty test"); + } + else { + this.console.error("fail:", "Empty test") + } + + this.failed++; + this.test.failed++; + } + + let wins = windows(null, { includePrivate: true }); + let winPromises = wins.map(win => { + return new Promise(resolve => { + if (["interactive", "complete"].indexOf(win.document.readyState) >= 0) { + resolve() + } + else { + win.addEventListener("DOMContentLoaded", function onLoad() { + win.removeEventListener("DOMContentLoaded", onLoad, false); + resolve(); + }, false); + } + }); + }); + + PromiseDebugging.flushUncaughtErrors(); + PromiseDebugging.removeUncaughtErrorObserver(this._uncaughtErrorObserver); + + + return all(winPromises).then(() => { + let browserWins = wins.filter(isBrowser); + let tabs = browserWins.reduce((tabs, window) => tabs.concat(getTabs(window)), []); + let newTabID = getTabId(getSelectedTab(wins[0])); + let oldTabID = runnerTabs.get(this); + let hasMoreTabsOpen = browserWins.length && tabs.length != 1; + let failure = false; + + 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)) { + tabs = getTabs(win); + console.log(win.location + " - " + tabs.map(getURI).join(", ")); + } + else { + console.log(win.location); + } + } + } + + return failure; + }). + then(failure => { + if (!failure) { + this.pass("There was a clean UI."); + return null; + } + return cleanUI().then(() => { + this.pass("There is a clean UI."); + }); + }). + then(() => { + this.testRunSummary.push({ + name: this.test.name, + passed: this.test.passed, + failed: this.test.failed, + errors: Object.keys(this.test.errors).join(", ") + }); + + if (this.onDone !== null) { + let onDone = this.onDone; + this.onDone = null; + timer.setTimeout(_ => onDone(this)); + } + }). + catch(console.exception); + }, + + // Set of assertion functions to wait for an assertion to become true + // These functions take the same arguments as the TestRunner.assert* methods. + waitUntil: function waitUntil() { + return this._waitUntil(this.assert, arguments); + }, + + waitUntilNotEqual: function waitUntilNotEqual() { + return this._waitUntil(this.assertNotEqual, arguments); + }, + + waitUntilEqual: function waitUntilEqual() { + return this._waitUntil(this.assertEqual, arguments); + }, + + waitUntilMatches: function waitUntilMatches() { + return this._waitUntil(this.assertMatches, arguments); + }, + + /** + * Internal function that waits for an assertion to become true. + * @param {Function} assertionMethod + * Reference to a TestRunner assertion method like test.assert, + * test.assertEqual, ... + * @param {Array} args + * List of arguments to give to the previous assertion method. + * All functions in this list are going to be called to retrieve current + * assertion values. + */ + _waitUntil: function waitUntil(assertionMethod, args) { + let { promise, resolve } = defer(); + let count = 0; + let maxCount = this.DEFAULT_PAUSE_TIMEOUT / this.PAUSE_DELAY; + + // We need to ensure that test is asynchronous + if (!this.waitTimeout) + this.waitUntilDone(this.DEFAULT_PAUSE_TIMEOUT); + + let finished = false; + let test = this; + + // capture a traceback before we go async. + let traceback = require("../console/traceback"); + let stack = traceback.get(); + stack.splice(-2, 2); + let currentWaitStack = traceback.format(stack); + let timeout = null; + + function loop(stopIt) { + timeout = null; + + // Build a mockup object to fake TestRunner API and intercept calls to + // pass and fail methods, in order to retrieve nice error messages + // and assertion result + let mock = { + pass: function (msg) { + test.pass(msg); + test.waitUntilCallback = null; + if (!stopIt) + resolve(); + }, + fail: function (msg) { + // If we are called on test timeout, we stop the loop + // and print which test keeps failing: + if (stopIt) { + test.console.error("test assertion never became true:\n", + msg + "\n", + currentWaitStack); + if (timeout) + timer.clearTimeout(timeout); + return; + } + timeout = timer.setTimeout(loop, test.PAUSE_DELAY); + } + }; + + // Automatically call args closures in order to build arguments for + // assertion function + let appliedArgs = []; + for (let i = 0, l = args.length; i < l; i++) { + let a = args[i]; + if (typeof a == "function") { + try { + a = a(); + } + catch(e) { + test.fail("Exception when calling asynchronous assertion: " + e + + "\n" + e.stack); + return resolve(); + } + } + appliedArgs.push(a); + } + + // Finally call assertion function with current assertion values + assertionMethod.apply(mock, appliedArgs); + } + loop(); + this.waitUntilCallback = loop; + + return promise; + }, + + waitUntilDone: function waitUntilDone(ms) { + if (ms === undefined) + ms = this.DEFAULT_PAUSE_TIMEOUT; + + var self = this; + + function tiredOfWaiting() { + self._logTestFailed("timed out"); + if ("testMessage" in self.console) { + self.console.testMessage(false, false, self.test.name, + `Test timed out (after: ${self.test.last})`); + } + else { + self.console.error("fail:", `Timed out (after: ${self.test.last})`) + } + if (self.waitUntilCallback) { + self.waitUntilCallback(true); + self.waitUntilCallback = null; + } + self.failed++; + self.test.failed++; + self.done(); + } + + // We may already have registered a timeout callback + if (this.waitTimeout) + timer.clearTimeout(this.waitTimeout); + + this.waitTimeout = timer.setTimeout(tiredOfWaiting, ms); + }, + + startMany: function startMany(options) { + function runNextTest(self) { + let { tests, onDone } = options; + + return tests.getNext().then((test) => { + if (options.stopOnError && self.test && self.test.failed) { + self.console.error("aborted: test failed and --stop-on-error was specified"); + onDone(self); + } + else if (test) { + self.start({test: test, onDone: runNextTest}); + } + else { + onDone(self); + } + }); + } + + return runNextTest(this).catch(console.exception); + }, + + start: function start(options) { + this.test = options.test; + this.test.passed = 0; + this.test.failed = 0; + this.test.errors = {}; + this.test.last = 'START'; + PromiseDebugging.clearUncaughtErrorObservers(); + this._uncaughtErrorObserver = this._uncaughtErrorObserver.bind(this); + PromiseDebugging.addUncaughtErrorObserver(this._uncaughtErrorObserver); + + this.isDone = false; + this.onDone = function(self) { + if (cfxArgs.parseable) + self.console.print("TEST-END | " + self.test.name + "\n"); + options.onDone(self); + } + this.waitTimeout = null; + + try { + if (cfxArgs.parseable) + this.console.print("TEST-START | " + this.test.name + "\n"); + else + this.console.info("executing '" + this.test.name + "'"); + + if(this.test.setup) { + this.test.setup(this); + } + this.test.testFunction(this); + } catch (e) { + this.exception(e); + } + if (this.waitTimeout === null) + this.done(); + } +}; +exports.TestRunner = TestRunner; diff --git a/addon-sdk/source/lib/sdk/deprecated/window-utils.js b/addon-sdk/source/lib/sdk/deprecated/window-utils.js new file mode 100644 index 000000000..93c0ab7b8 --- /dev/null +++ b/addon-sdk/source/lib/sdk/deprecated/window-utils.js @@ -0,0 +1,193 @@ +/* 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': 'deprecated' +}; + +const { Cc, Ci } = require('chrome'); +const events = require('../system/events'); +const { getInnerId, getOuterId, windows, isDocumentLoaded, isBrowser, + getMostRecentBrowserWindow, getToplevelWindow, getMostRecentWindow } = require('../window/utils'); +const { deprecateFunction } = require('../util/deprecate'); +const { ignoreWindow } = require('sdk/private-browsing/utils'); +const { isPrivateBrowsingSupported } = require('../self'); + +const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1']. + getService(Ci.nsIWindowWatcher); +const appShellService = Cc['@mozilla.org/appshell/appShellService;1']. + getService(Ci.nsIAppShellService); + +// Bug 834961: ignore private windows when they are not supported +function getWindows() { + return windows(null, { includePrivate: isPrivateBrowsingSupported }); +} + +/** + * An iterator for XUL windows currently in the application. + * + * @return A generator that yields XUL windows exposing the + * nsIDOMWindow interface. + */ +function windowIterator() { + // Bug 752631: We only pass already loaded window in order to avoid + // breaking XUL windows DOM. DOM is broken when some JS code try + // to access DOM during "uninitialized" state of the related document. + let list = getWindows().filter(isDocumentLoaded); + for (let i = 0, l = list.length; i < l; i++) { + yield list[i]; + } +}; +exports.windowIterator = windowIterator; + +/** + * An iterator for browser windows currently open in the application. + * @returns {Function} + * A generator that yields browser windows exposing the `nsIDOMWindow` + * interface. + */ +function browserWindowIterator() { + for (let window of windowIterator()) { + if (isBrowser(window)) + yield window; + } +} +exports.browserWindowIterator = browserWindowIterator; + +function WindowTracker(delegate) { + if (!(this instanceof WindowTracker)) { + return new WindowTracker(delegate); + } + + this._delegate = delegate; + + for (let window of getWindows()) + this._regWindow(window); + windowWatcher.registerNotification(this); + this._onToplevelWindowReady = this._onToplevelWindowReady.bind(this); + events.on('toplevel-window-ready', this._onToplevelWindowReady); + + require('../system/unload').ensure(this); + + return this; +}; + +WindowTracker.prototype = { + _regLoadingWindow: function _regLoadingWindow(window) { + // Bug 834961: ignore private windows when they are not supported + if (ignoreWindow(window)) + return; + + window.addEventListener('load', this, true); + }, + + _unregLoadingWindow: function _unregLoadingWindow(window) { + // This may have no effect if we ignored the window in _regLoadingWindow(). + window.removeEventListener('load', this, true); + }, + + _regWindow: function _regWindow(window) { + // Bug 834961: ignore private windows when they are not supported + if (ignoreWindow(window)) + return; + + if (window.document.readyState == 'complete') { + this._unregLoadingWindow(window); + this._delegate.onTrack(window); + } else + this._regLoadingWindow(window); + }, + + _unregWindow: function _unregWindow(window) { + if (window.document.readyState == 'complete') { + if (this._delegate.onUntrack) + this._delegate.onUntrack(window); + } else { + this._unregLoadingWindow(window); + } + }, + + unload: function unload() { + windowWatcher.unregisterNotification(this); + events.off('toplevel-window-ready', this._onToplevelWindowReady); + for (let window of getWindows()) + this._unregWindow(window); + }, + + handleEvent: function handleEvent(event) { + try { + if (event.type == 'load' && event.target) { + var window = event.target.defaultView; + if (window) + this._regWindow(getToplevelWindow(window)); + } + } + catch(e) { + console.exception(e); + } + }, + + _onToplevelWindowReady: function _onToplevelWindowReady({subject}) { + let window = getToplevelWindow(subject); + // ignore private windows if they are not supported + if (ignoreWindow(window)) + return; + this._regWindow(window); + }, + + observe: function observe(subject, topic, data) { + try { + var window = subject.QueryInterface(Ci.nsIDOMWindow); + // ignore private windows if they are not supported + if (ignoreWindow(window)) + return; + if (topic == 'domwindowclosed') + this._unregWindow(window); + } + catch(e) { + console.exception(e); + } + } +}; +exports.WindowTracker = WindowTracker; + +Object.defineProperties(exports, { + activeWindow: { + enumerable: true, + get: function() { + return getMostRecentWindow(null); + }, + set: function(window) { + try { + window.focus(); + } catch (e) {} + } + }, + activeBrowserWindow: { + enumerable: true, + get: getMostRecentBrowserWindow + } +}); + + +/** + * Returns the ID of the window's current inner window. + */ +exports.getInnerId = deprecateFunction(getInnerId, + 'require("window-utils").getInnerId is deprecated, ' + + 'please use require("sdk/window/utils").getInnerId instead' +); + +exports.getOuterId = deprecateFunction(getOuterId, + 'require("window-utils").getOuterId is deprecated, ' + + 'please use require("sdk/window/utils").getOuterId instead' +); + +exports.isBrowser = deprecateFunction(isBrowser, + 'require("window-utils").isBrowser is deprecated, ' + + 'please use require("sdk/window/utils").isBrowser instead' +); + +exports.hiddenWindow = appShellService.hiddenDOMWindow; diff --git a/addon-sdk/source/lib/sdk/dom/events-shimmed.js b/addon-sdk/source/lib/sdk/dom/events-shimmed.js new file mode 100644 index 000000000..7a1727681 --- /dev/null +++ b/addon-sdk/source/lib/sdk/dom/events-shimmed.js @@ -0,0 +1,18 @@ +/* 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 events = require('./events.js'); + +exports.emit = (element, type, obj) => events.emit(element, type, obj, true); +exports.on = (element, type, listener, capture) => events.on(element, type, listener, capture, true); +exports.once = (element, type, listener, capture) => events.once(element, type, listener, capture, true); +exports.removeListener = (element, type, listener, capture) => events.removeListener(element, type, listener, capture, true); +exports.removed = events.removed; +exports.when = (element, eventName, capture) => events.when(element, eventName, capture ? capture : false, true); diff --git a/addon-sdk/source/lib/sdk/dom/events.js b/addon-sdk/source/lib/sdk/dom/events.js new file mode 100644 index 000000000..502d2350f --- /dev/null +++ b/addon-sdk/source/lib/sdk/dom/events.js @@ -0,0 +1,192 @@ +/* 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 { Cu } = require("chrome"); +const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm"); + +// Utility function that returns copy of the given `text` with last character +// removed if it is `"s"`. +function singularify(text) { + return text[text.length - 1] === "s" ? text.substr(0, text.length - 1) : text; +} + +// Utility function that takes event type, argument is passed to +// `document.createEvent` and returns name of the initializer method of the +// given event. Please note that there are some event types whose initializer +// methods can't be guessed by this function. For more details see following +// link: https://developer.mozilla.org/En/DOM/Document.createEvent +function getInitializerName(category) { + return "init" + singularify(category); +} + +/** + * Registers an event `listener` on a given `element`, that will be called + * when events of specified `type` is dispatched on the `element`. + * @param {Element} element + * Dom element to register listener on. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type) to + * listen for. + * @param {Function} listener + * Function that is called whenever an event of the specified `type` + * occurs. + * @param {Boolean} capture + * If true, indicates that the user wishes to initiate capture. After + * initiating capture, all events of the specified type will be dispatched + * to the registered listener before being dispatched to any `EventTarget`s + * beneath it in the DOM tree. Events which are bubbling upward through + * the tree will not trigger a listener designated to use capture. + * See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow) + * for a detailed explanation. + */ +function on(element, type, listener, capture, shimmed = false) { + // `capture` defaults to `false`. + capture = capture || false; + if (shimmed) { + element.addEventListener(type, listener, capture); + } else { + ShimWaiver.getProperty(element, "addEventListener")(type, listener, capture); + } +} +exports.on = on; + +/** + * Registers an event `listener` on a given `element`, that will be called + * only once, next time event of specified `type` is dispatched on the + * `element`. + * @param {Element} element + * Dom element to register listener on. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type) to + * listen for. + * @param {Function} listener + * Function that is called whenever an event of the specified `type` + * occurs. + * @param {Boolean} capture + * If true, indicates that the user wishes to initiate capture. After + * initiating capture, all events of the specified type will be dispatched + * to the registered listener before being dispatched to any `EventTarget`s + * beneath it in the DOM tree. Events which are bubbling upward through + * the tree will not trigger a listener designated to use capture. + * See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow) + * for a detailed explanation. + */ +function once(element, type, listener, capture, shimmed = false) { + on(element, type, function selfRemovableListener(event) { + removeListener(element, type, selfRemovableListener, capture, shimmed); + listener.apply(this, arguments); + }, capture, shimmed); +} +exports.once = once; + +/** + * Unregisters an event `listener` on a given `element` for the events of the + * specified `type`. + * + * @param {Element} element + * Dom element to unregister listener from. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type) to + * listen for. + * @param {Function} listener + * Function that is called whenever an event of the specified `type` + * occurs. + * @param {Boolean} capture + * If true, indicates that the user wishes to initiate capture. After + * initiating capture, all events of the specified type will be dispatched + * to the registered listener before being dispatched to any `EventTarget`s + * beneath it in the DOM tree. Events which are bubbling upward through + * the tree will not trigger a listener designated to use capture. + * See [DOM Level 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#event-flow) + * for a detailed explanation. + */ +function removeListener(element, type, listener, capture, shimmed = false) { + if (shimmed) { + element.removeEventListener(type, listener, capture); + } else { + ShimWaiver.getProperty(element, "removeEventListener")(type, listener, capture); + } +} +exports.removeListener = removeListener; + +/** + * Emits event of the specified `type` and `category` on the given `element`. + * Specified `settings` are used to initialize event before dispatching it. + * @param {Element} element + * Dom element to dispatch event on. + * @param {String} type + * A string representing the + * [event type](https://developer.mozilla.org/en/DOM/event.type). + * @param {Object} options + * Options object containing following properties: + * - `category`: String passed to the `document.createEvent`. Option is + * optional and defaults to "UIEvents". + * - `initializer`: If passed it will be used as name of the method used + * to initialize event. If omitted name will be generated from the + * `category` field by prefixing it with `"init"` and removing last + * character if it matches `"s"`. + * - `settings`: Array of settings that are forwarded to the event + * initializer after firs `type` argument. + * @see https://developer.mozilla.org/En/DOM/Document.createEvent + */ +function emit(element, type, { category, initializer, settings }, shimmed = false) { + category = category || "UIEvents"; + initializer = initializer || getInitializerName(category); + let document = element.ownerDocument; + let event = document.createEvent(category); + event[initializer].apply(event, [type].concat(settings)); + if (shimmed) { + element.dispatchEvent(event); + } else { + ShimWaiver.getProperty(element, "dispatchEvent")(event); + } +}; +exports.emit = emit; + +// Takes DOM `element` and returns promise which is resolved +// when given element is removed from it's parent node. +const removed = element => { + return new Promise(resolve => { + const { MutationObserver } = element.ownerDocument.defaultView; + const observer = new MutationObserver(mutations => { + for (let mutation of mutations) { + for (let node of mutation.removedNodes || []) { + if (node === element) { + observer.disconnect(); + resolve(element); + } + } + } + }); + observer.observe(element.parentNode, {childList: true}); + }); +}; +exports.removed = removed; + +const when = (element, eventName, capture=false, shimmed=false) => new Promise(resolve => { + const listener = event => { + if (shimmed) { + element.removeEventListener(eventName, listener, capture); + } else { + ShimWaiver.getProperty(element, "removeEventListener")(eventName, listener, capture); + } + resolve(event); + }; + + if (shimmed) { + element.addEventListener(eventName, listener, capture); + } else { + ShimWaiver.getProperty(element, "addEventListener")(eventName, listener, capture); + } +}); +exports.when = when; diff --git a/addon-sdk/source/lib/sdk/dom/events/keys.js b/addon-sdk/source/lib/sdk/dom/events/keys.js new file mode 100644 index 000000000..e6f1483a2 --- /dev/null +++ b/addon-sdk/source/lib/sdk/dom/events/keys.js @@ -0,0 +1,63 @@ +/* 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("../events"); +const { getCodeForKey, toJSON } = require("../../keyboard/utils"); +const { has } = require("../../util/array"); +const { isString } = require("../../lang/type"); + +const INITIALIZER = "initKeyEvent"; +const CATEGORY = "KeyboardEvent"; + +function Options(options) { + if (!isString(options)) + return options; + + var { key, modifiers } = toJSON(options); + return { + key: key, + control: has(modifiers, "control"), + alt: has(modifiers, "alt"), + shift: has(modifiers, "shift"), + meta: has(modifiers, "meta") + }; +} + +var keyEvent = exports.keyEvent = function keyEvent(element, type, options) { + + emit(element, type, { + initializer: INITIALIZER, + category: CATEGORY, + settings: [ + !("bubbles" in options) || options.bubbles !== false, + !("cancelable" in options) || options.cancelable !== false, + "window" in options && options.window ? options.window : null, + "control" in options && !!options.control, + "alt" in options && !!options.alt, + "shift" in options && !!options.shift, + "meta" in options && !!options.meta, + getCodeForKey(options.key) || 0, + options.key.length === 1 ? options.key.charCodeAt(0) : 0 + ] + }); +} + +exports.keyDown = function keyDown(element, options) { + keyEvent(element, "keydown", Options(options)); +}; + +exports.keyUp = function keyUp(element, options) { + keyEvent(element, "keyup", Options(options)); +}; + +exports.keyPress = function keyPress(element, options) { + keyEvent(element, "keypress", Options(options)); +}; + diff --git a/addon-sdk/source/lib/sdk/event/chrome.js b/addon-sdk/source/lib/sdk/event/chrome.js new file mode 100644 index 000000000..9044fef99 --- /dev/null +++ b/addon-sdk/source/lib/sdk/event/chrome.js @@ -0,0 +1,65 @@ +/* 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, Cr, Cu } = require("chrome"); +const { emit, on, off } = require("./core"); +var observerService = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + +const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm"); +const addObserver = ShimWaiver.getProperty(observerService, "addObserver"); +const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver"); + +const { when: unload } = require("../system/unload"); + +// Simple class that can be used to instantiate event channel that +// implements `nsIObserver` interface. It's will is used by `observe` +// function as observer + event target. It basically proxies observer +// notifications as to it's registered listeners. +function ObserverChannel() {} +Object.freeze(Object.defineProperties(ObserverChannel.prototype, { + QueryInterface: { + value: function(iid) { + if (!iid.equals(Ci.nsIObserver) && + !iid.equals(Ci.nsISupportsWeakReference) && + !iid.equals(Ci.nsISupports)) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + } + }, + observe: { + value: function(subject, topic, data) { + emit(this, "data", { + type: topic, + target: subject, + data: data + }); + } + } +})); + +function observe(topic) { + let observerChannel = new ObserverChannel(); + + // Note: `nsIObserverService` will not hold a weak reference to a + // observerChannel (since third argument is `true`). There for if it + // will be GC-ed with all it's event listeners once no other references + // will be held. + addObserver(observerChannel, topic, true); + + // We need to remove any observer added once the add-on is unloaded; + // otherwise we'll get a "dead object" exception. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1001833 + unload(() => removeObserver(observerChannel, topic)); + + return observerChannel; +} + +exports.observe = observe; diff --git a/addon-sdk/source/lib/sdk/event/core.js b/addon-sdk/source/lib/sdk/event/core.js new file mode 100644 index 000000000..c16dd2df5 --- /dev/null +++ b/addon-sdk/source/lib/sdk/event/core.js @@ -0,0 +1,193 @@ +/* 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 UNCAUGHT_ERROR = 'An error event was emitted for which there was no listener.'; +const BAD_LISTENER = 'The event listener must be a function.'; + +const { ns } = require('../core/namespace'); + +const event = ns(); + +const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/; +exports.EVENT_TYPE_PATTERN = EVENT_TYPE_PATTERN; + +// Utility function to access given event `target` object's event listeners for +// the specific event `type`. If listeners for this type does not exists they +// will be created. +const observers = function observers(target, type) { + if (!target) throw TypeError("Event target must be an object"); + let listeners = event(target); + return type in listeners ? listeners[type] : listeners[type] = []; +}; + +/** + * Registers an event `listener` that is called every time events of + * specified `type` is emitted on the given event `target`. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ +function on(target, type, listener) { + if (typeof(listener) !== 'function') + throw new Error(BAD_LISTENER); + + let listeners = observers(target, type); + if (!~listeners.indexOf(listener)) + listeners.push(listener); +} +exports.on = on; + + +var onceWeakMap = new WeakMap(); + + +/** + * Registers an event `listener` that is called only the next time an event + * of the specified `type` is emitted on the given event `target`. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of the event. + * @param {Function} listener + * The listener function that processes the event. + */ +function once(target, type, listener) { + let replacement = function observer(...args) { + off(target, type, observer); + onceWeakMap.delete(listener); + listener.apply(target, args); + }; + onceWeakMap.set(listener, replacement); + on(target, type, replacement); +} +exports.once = once; + +/** + * Execute each of the listeners in order with the supplied arguments. + * All the exceptions that are thrown by listeners during the emit + * are caught and can be handled by listeners of 'error' event. Thrown + * exceptions are passed as an argument to an 'error' event listener. + * If no 'error' listener is registered exception will be logged into an + * error console. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of event. + * @params {Object|Number|String|Boolean} args + * Arguments that will be passed to listeners. + */ +function emit (target, type, ...args) { + emitOnObject(target, type, target, ...args); +} +exports.emit = emit; + +/** + * A variant of emit that allows setting the this property for event listeners + */ +function emitOnObject(target, type, thisArg, ...args) { + let all = observers(target, '*').length; + let state = observers(target, type); + let listeners = state.slice(); + let count = listeners.length; + let index = 0; + + // If error event and there are no handlers (explicit or catch-all) + // then print error message to the console. + if (count === 0 && type === 'error' && all === 0) + console.exception(args[0]); + while (index < count) { + try { + let listener = listeners[index]; + // Dispatch only if listener is still registered. + if (~state.indexOf(listener)) + listener.apply(thisArg, args); + } + catch (error) { + // If exception is not thrown by a error listener and error listener is + // registered emit `error` event. Otherwise dump exception to the console. + if (type !== 'error') emit(target, 'error', error); + else console.exception(error); + } + index++; + } + // Also emit on `"*"` so that one could listen for all events. + if (type !== '*') emit(target, '*', type, ...args); +} +exports.emitOnObject = emitOnObject; + +/** + * Removes an event `listener` for the given event `type` on the given event + * `target`. If no `listener` is passed removes all listeners of the given + * `type`. If `type` is not passed removes all the listeners of the given + * event `target`. + * @param {Object} target + * The event target object. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ +function off(target, type, listener) { + let length = arguments.length; + if (length === 3) { + if (onceWeakMap.has(listener)) { + listener = onceWeakMap.get(listener); + onceWeakMap.delete(listener); + } + + let listeners = observers(target, type); + let index = listeners.indexOf(listener); + if (~index) + listeners.splice(index, 1); + } + else if (length === 2) { + observers(target, type).splice(0); + } + else if (length === 1) { + let listeners = event(target); + Object.keys(listeners).forEach(type => delete listeners[type]); + } +} +exports.off = off; + +/** + * Returns a number of event listeners registered for the given event `type` + * on the given event `target`. + */ +function count(target, type) { + return observers(target, type).length; +} +exports.count = count; + +/** + * Registers listeners on the given event `target` from the given `listeners` + * dictionary. Iterates over the listeners and if property name matches name + * pattern `onEventType` and property is a function, then registers it as + * an `eventType` listener on `target`. + * + * @param {Object} target + * The type of event. + * @param {Object} listeners + * Dictionary of listeners. + */ +function setListeners(target, listeners) { + Object.keys(listeners || {}).forEach(key => { + let match = EVENT_TYPE_PATTERN.exec(key); + let type = match && match[1].toLowerCase(); + if (!type) return; + + let listener = listeners[key]; + if (typeof(listener) === 'function') + on(target, type, listener); + }); +} +exports.setListeners = setListeners; diff --git a/addon-sdk/source/lib/sdk/event/dom.js b/addon-sdk/source/lib/sdk/event/dom.js new file mode 100644 index 000000000..da99dec7a --- /dev/null +++ b/addon-sdk/source/lib/sdk/event/dom.js @@ -0,0 +1,78 @@ +/* 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"); + +var { emit } = require("./core"); +var { when: unload } = require("../system/unload"); +var listeners = new WeakMap(); + +const { Cu } = require("chrome"); +const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm"); +const { ThreadSafeChromeUtils } = Cu.import("resource://gre/modules/Services.jsm", {}); + +var getWindowFrom = x => + x instanceof Ci.nsIDOMWindow ? x : + x instanceof Ci.nsIDOMDocument ? x.defaultView : + x instanceof Ci.nsIDOMNode ? x.ownerDocument.defaultView : + null; + +function removeFromListeners() { + ShimWaiver.getProperty(this, "removeEventListener")("DOMWindowClose", removeFromListeners); + for (let cleaner of listeners.get(this)) + cleaner(); + + listeners.delete(this); +} + +// Simple utility function takes event target, event type and optional +// `options.capture` and returns node style event stream that emits "data" +// events every time event of that type occurs on the given `target`. +function open(target, type, options) { + let output = {}; + let capture = options && options.capture ? true : false; + let listener = (event) => emit(output, "data", event); + + // `open` is currently used only on DOM Window objects, however it was made + // to be used to any kind of `target` that supports `addEventListener`, + // therefore is safer get the `window` from the `target` instead assuming + // that `target` is the `window`. + let window = getWindowFrom(target); + + // If we're not able to get a `window` from `target`, there is something + // wrong. We cannot add listeners that can leak later, or results in + // "dead object" exception. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1001833 + if (!window) + throw new Error("Unable to obtain the owner window from the target given."); + + let cleaners = listeners.get(window); + if (!cleaners) { + cleaners = []; + listeners.set(window, cleaners); + + // We need to remove from our map the `window` once is closed, to prevent + // memory leak + ShimWaiver.getProperty(window, "addEventListener")("DOMWindowClose", removeFromListeners); + } + + cleaners.push(() => ShimWaiver.getProperty(target, "removeEventListener")(type, listener, capture)); + ShimWaiver.getProperty(target, "addEventListener")(type, listener, capture); + + return output; +} + +unload(() => { + let keys = ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(listeners) + for (let window of keys) + removeFromListeners.call(window); +}); + +exports.open = open; diff --git a/addon-sdk/source/lib/sdk/event/target.js b/addon-sdk/source/lib/sdk/event/target.js new file mode 100644 index 000000000..3a1f5e5f0 --- /dev/null +++ b/addon-sdk/source/lib/sdk/event/target.js @@ -0,0 +1,74 @@ +/* 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": "stable" +}; + +const { on, once, off, setListeners } = require('./core'); +const { method, chainable } = require('../lang/functional/core'); +const { Class } = require('../core/heritage'); + +/** + * `EventTarget` is an exemplar for creating an objects that can be used to + * add / remove event listeners on them. Events on these objects may be emitted + * via `emit` function exported by 'event/core' module. + */ +const EventTarget = Class({ + /** + * Method initializes `this` event source. It goes through properties of a + * given `options` and registers listeners for the ones that look like an + * event listeners. + */ + /** + * Method initializes `this` event source. It goes through properties of a + * given `options` and registers listeners for the ones that look like an + * event listeners. + */ + initialize: function initialize(options) { + setListeners(this, options); + }, + /** + * Registers an event `listener` that is called every time events of + * specified `type` are emitted. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + * @example + * worker.on('message', function (data) { + * console.log('data received: ' + data) + * }) + */ + on: chainable(method(on)), + /** + * Registers an event `listener` that is called once the next time an event + * of the specified `type` is emitted. + * @param {String} type + * The type of the event. + * @param {Function} listener + * The listener function that processes the event. + */ + once: chainable(method(once)), + /** + * Removes an event `listener` for the given event `type`. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ + removeListener: function removeListener(type, listener) { + // Note: We can't just wrap `off` in `method` as we do it for other methods + // cause skipping a second or third argument will behave very differently + // than intended. This way we make sure all arguments are passed and only + // one listener is removed at most. + off(this, type, listener); + return this; + }, + // but we can wrap `off` here, as the semantics are the same + off: chainable(method(off)) + +}); +exports.EventTarget = EventTarget; diff --git a/addon-sdk/source/lib/sdk/event/utils.js b/addon-sdk/source/lib/sdk/event/utils.js new file mode 100644 index 000000000..f193b6785 --- /dev/null +++ b/addon-sdk/source/lib/sdk/event/utils.js @@ -0,0 +1,328 @@ +/* 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" +}; + +var { emit, on, once, off, EVENT_TYPE_PATTERN } = require("./core"); +const { Cu } = require("chrome"); + +// This module provides set of high order function for working with event +// streams (streams in a NodeJS style that dispatch data, end and error +// events). + +// Function takes a `target` object and returns set of implicit references +// (non property references) it keeps. This basically allows defining +// references between objects without storing the explicitly. See transform for +// more details. +var refs = (function() { + let refSets = new WeakMap(); + return function refs(target) { + if (!refSets.has(target)) refSets.set(target, new Set()); + return refSets.get(target); + }; +})(); + +function transform(input, f) { + let output = new Output(); + + // Since event listeners don't prevent `input` to be GC-ed we wanna presrve + // it until `output` can be GC-ed. There for we add implicit reference which + // is removed once `input` ends. + refs(output).add(input); + + const next = data => receive(output, data); + once(output, "start", () => start(input)); + on(input, "error", error => emit(output, "error", error)); + on(input, "end", function() { + refs(output).delete(input); + end(output); + }); + on(input, "data", data => f(data, next)); + return output; +} + +// High order event transformation function that takes `input` event channel +// and returns transformation containing only events on which `p` predicate +// returns `true`. +function filter(input, predicate) { + return transform(input, function(data, next) { + if (predicate(data)) + next(data); + }); +} +exports.filter = filter; + +// High order function that takes `input` and returns input of it's values +// mapped via given `f` function. +const map = (input, f) => transform(input, (data, next) => next(f(data))); +exports.map = map; + +// High order function that takes `input` stream of streams and merges them +// into single event stream. Like flatten but time based rather than order +// based. +function merge(inputs) { + let output = new Output(); + let open = 1; + let state = []; + output.state = state; + refs(output).add(inputs); + + function end(input) { + open = open - 1; + refs(output).delete(input); + if (open === 0) emit(output, "end"); + } + const error = e => emit(output, "error", e); + function forward(input) { + state.push(input); + open = open + 1; + on(input, "end", () => end(input)); + on(input, "error", error); + on(input, "data", data => emit(output, "data", data)); + } + + // If `inputs` is an array treat it as a stream. + if (Array.isArray(inputs)) { + inputs.forEach(forward); + end(inputs); + } + else { + on(inputs, "end", () => end(inputs)); + on(inputs, "error", error); + on(inputs, "data", forward); + } + + return output; +} +exports.merge = merge; + +const expand = (inputs, f) => merge(map(inputs, f)); +exports.expand = expand; + +const pipe = (from, to) => on(from, "*", emit.bind(emit, to)); +exports.pipe = pipe; + + +// Shim signal APIs so other modules can be used as is. +const receive = (input, message) => { + if (input[receive]) + input[receive](input, message); + else + emit(input, "data", message); + + // Ideally our input will extend Input and already provide a weak value + // getter. If not, opportunistically shim the weak value getter on + // other types passed as the input. + if (!("value" in input)) { + Object.defineProperty(input, "value", WeakValueGetterSetter); + } + input.value = message; +}; +receive.toString = () => "@@receive"; +exports.receive = receive; +exports.send = receive; + +const end = input => { + if (input[end]) + input[end](input); + else + emit(input, "end", input); +}; +end.toString = () => "@@end"; +exports.end = end; + +const stop = input => { + if (input[stop]) + input[stop](input); + else + emit(input, "stop", input); +}; +stop.toString = () => "@@stop"; +exports.stop = stop; + +const start = input => { + if (input[start]) + input[start](input); + else + emit(input, "start", input); +}; +start.toString = () => "@@start"; +exports.start = start; + +const lift = (step, ...inputs) => { + let args = null; + let opened = inputs.length; + let started = false; + const output = new Output(); + const init = () => { + args = [...inputs.map(input => input.value)]; + output.value = step(...args); + }; + + inputs.forEach((input, index) => { + on(input, "data", data => { + args[index] = data; + receive(output, step(...args)); + }); + on(input, "end", () => { + opened = opened - 1; + if (opened <= 0) + end(output); + }); + }); + + once(output, "start", () => { + inputs.forEach(start); + init(); + }); + + init(); + + return output; +}; +exports.lift = lift; + +const merges = inputs => { + let opened = inputs.length; + let output = new Output(); + output.value = inputs[0].value; + inputs.forEach((input, index) => { + on(input, "data", data => receive(output, data)); + on(input, "end", () => { + opened = opened - 1; + if (opened <= 0) + end(output); + }); + }); + + once(output, "start", () => { + inputs.forEach(start); + output.value = inputs[0].value; + }); + + return output; +}; +exports.merges = merges; + +const foldp = (step, initial, input) => { + let output = map(input, x => step(output.value, x)); + output.value = initial; + return output; +}; +exports.foldp = foldp; + +const keepIf = (p, base, input) => { + let output = filter(input, p); + output.value = base; + return output; +}; +exports.keepIf = keepIf; + +function Input() {} +Input.start = input => emit(input, "start", input); +Input.prototype.start = Input.start; + +Input.end = input => { + emit(input, "end", input); + stop(input); +}; +Input.prototype[end] = Input.end; + +// The event channel system caches the last event seen as input.value. +// Unfortunately, if the last event is a DOM object this is a great way +// leak windows. Mitigate this by storing input.value using a weak +// reference. This allows the system to work for normal event processing +// while also allowing the objects to be reclaimed. It means, however, +// input.value cannot be accessed long after the event was dispatched. +const WeakValueGetterSetter = { + get: function() { + return this._weakValue ? this._weakValue.get() : this._simpleValue + }, + set: function(v) { + if (v && typeof v === "object") { + try { + // Try to set a weak reference. This can throw for some values. + // For example, if the value is a native object that does not + // implement nsISupportsWeakReference. + this._weakValue = Cu.getWeakReference(v) + this._simpleValue = undefined; + return; + } catch (e) { + // Do nothing. Fall through to setting _simpleValue below. + } + } + this._simpleValue = v; + this._weakValue = undefined; + }, +} +Object.defineProperty(Input.prototype, "value", WeakValueGetterSetter); + +exports.Input = Input; + +// Define an Output type with a weak value getter for the transformation +// functions that produce new channels. +function Output() { } +Object.defineProperty(Output.prototype, "value", WeakValueGetterSetter); +exports.Output = Output; + +const $source = "@@source"; +const $outputs = "@@outputs"; +exports.outputs = $outputs; + +// NOTE: Passing DOM objects through a Reactor can cause them to leak +// when they get cached in this.value. We cannot use a weak reference +// in this case because the Reactor design expects to always have both the +// past and present value. If we allow past values to be collected the +// system breaks. + +function Reactor(options={}) { + const {onStep, onStart, onEnd} = options; + if (onStep) + this.onStep = onStep; + if (onStart) + this.onStart = onStart; + if (onEnd) + this.onEnd = onEnd; +} +Reactor.prototype.onStep = _ => void(0); +Reactor.prototype.onStart = _ => void(0); +Reactor.prototype.onEnd = _ => void(0); +Reactor.prototype.onNext = function(present, past) { + this.value = present; + this.onStep(present, past); +}; +Reactor.prototype.run = function(input) { + on(input, "data", message => this.onNext(message, input.value)); + on(input, "end", () => this.onEnd(input.value)); + start(input); + this.value = input.value; + this.onStart(input.value); +}; +exports.Reactor = Reactor; + +/** + * Takes an object used as options with potential keys like 'onMessage', + * used to be called `require('sdk/event/core').setListeners` on. + * This strips all keys that would trigger a listener to be set. + * + * @params {Object} object + * @return {Object} + */ + +function stripListeners (object) { + return Object.keys(object || {}).reduce((agg, key) => { + if (!EVENT_TYPE_PATTERN.test(key)) + agg[key] = object[key]; + return agg; + }, {}); +} +exports.stripListeners = stripListeners; + +const when = (target, type) => new Promise(resolve => { + once(target, type, resolve); +}); +exports.when = when; diff --git a/addon-sdk/source/lib/sdk/frame/hidden-frame.js b/addon-sdk/source/lib/sdk/frame/hidden-frame.js new file mode 100644 index 000000000..97e0b7974 --- /dev/null +++ b/addon-sdk/source/lib/sdk/frame/hidden-frame.js @@ -0,0 +1,115 @@ +/* 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 { Cc, Ci } = require("chrome"); +const { Class } = require("../core/heritage"); +const { List, addListItem, removeListItem } = require("../util/list"); +const { EventTarget } = require("../event/target"); +const { emit } = require("../event/core"); +const { create: makeFrame } = require("./utils"); +const { defer } = require("../core/promise"); +const { when: unload } = require("../system/unload"); +const { validateOptions, getTypeOf } = require("../deprecated/api-utils"); +const { window } = require("../addon/window"); +const { fromIterator } = require("../util/array"); + +// This cache is used to access friend properties between functions +// without exposing them on the public API. +var cache = new Set(); +var elements = new WeakMap(); + +function contentLoaded(target) { + var deferred = defer(); + target.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) { + // "DOMContentLoaded" events from nested frames propagate up to target, + // ignore events unless it's DOMContentLoaded for the given target. + if (event.target === target || event.target === target.contentDocument) { + target.removeEventListener("DOMContentLoaded", DOMContentLoaded, false); + deferred.resolve(target); + } + }, false); + return deferred.promise; +} + +function FrameOptions(options) { + options = options || {} + return validateOptions(options, FrameOptions.validator); +} +FrameOptions.validator = { + onReady: { + is: ["undefined", "function", "array"], + ok: function(v) { + if (getTypeOf(v) === "array") { + // make sure every item is a function + return v.every(item => typeof(item) === "function") + } + return true; + } + }, + onUnload: { + is: ["undefined", "function"] + } +}; + +var HiddenFrame = Class({ + extends: EventTarget, + initialize: function initialize(options) { + options = FrameOptions(options); + EventTarget.prototype.initialize.call(this, options); + }, + get element() { + return elements.get(this); + }, + toString: function toString() { + return "[object Frame]" + } +}); +exports.HiddenFrame = HiddenFrame + +function addHidenFrame(frame) { + if (!(frame instanceof HiddenFrame)) + throw Error("The object to be added must be a HiddenFrame."); + + // This instance was already added. + if (cache.has(frame)) return frame; + else cache.add(frame); + + let element = makeFrame(window.document, { + nodeName: "iframe", + type: "content", + allowJavascript: true, + allowPlugins: true, + allowAuth: true, + }); + elements.set(frame, element); + + contentLoaded(element).then(function onFrameReady(element) { + emit(frame, "ready"); + }, console.exception); + + return frame; +} +exports.add = addHidenFrame + +function removeHiddenFrame(frame) { + if (!(frame instanceof HiddenFrame)) + throw Error("The object to be removed must be a HiddenFrame."); + + if (!cache.has(frame)) return; + + // Remove from cache before calling in order to avoid loop + cache.delete(frame); + emit(frame, "unload") + let element = frame.element + if (element) element.parentNode.removeChild(element) +} +exports.remove = removeHiddenFrame; + +unload(() => fromIterator(cache).forEach(removeHiddenFrame)); diff --git a/addon-sdk/source/lib/sdk/frame/utils.js b/addon-sdk/source/lib/sdk/frame/utils.js new file mode 100644 index 000000000..d9fccec4d --- /dev/null +++ b/addon-sdk/source/lib/sdk/frame/utils.js @@ -0,0 +1,94 @@ +/* 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 { Ci } = require("chrome"); +const XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + +function eventTarget(frame) { + return getDocShell(frame).chromeEventHandler; +} +exports.eventTarget = eventTarget; + +function getDocShell(frame) { + let { frameLoader } = frame.QueryInterface(Ci.nsIFrameLoaderOwner); + return frameLoader && frameLoader.docShell; +} +exports.getDocShell = getDocShell; + +/** + * Creates a XUL `browser` element in a privileged document. + * @params {nsIDOMDocument} document + * @params {String} options.type + * By default is 'content' for possible values see: + * https://developer.mozilla.org/en/XUL/iframe#a-browser.type + * @params {String} options.uri + * URI of the document to be loaded into created frame. + * @params {Boolean} options.remote + * If `true` separate process will be used for this frame, also in such + * case all the following options are ignored. + * @params {Boolean} options.allowAuth + * Whether to allow auth dialogs. Defaults to `false`. + * @params {Boolean} options.allowJavascript + * Whether to allow Javascript execution. Defaults to `false`. + * @params {Boolean} options.allowPlugins + * Whether to allow plugin execution. Defaults to `false`. + */ +function create(target, options) { + target = target instanceof Ci.nsIDOMDocument ? target.documentElement : + target instanceof Ci.nsIDOMWindow ? target.document.documentElement : + target; + options = options || {}; + let remote = options.remote || false; + let namespaceURI = options.namespaceURI || XUL; + let isXUL = namespaceURI === XUL; + let nodeName = isXUL && options.browser ? 'browser' : 'iframe'; + let document = target.ownerDocument; + + let frame = document.createElementNS(namespaceURI, nodeName); + // Type="content" is mandatory to enable stuff here: + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1776 + frame.setAttribute('type', options.type || 'content'); + frame.setAttribute('src', options.uri || 'about:blank'); + + // Must set the remote attribute before attaching the frame to the document + if (remote && isXUL) { + // We remove XBL binding to avoid execution of code that is not going to + // work because browser has no docShell attribute in remote mode + // (for example) + frame.setAttribute('style', '-moz-binding: none;'); + frame.setAttribute('remote', 'true'); + } + + target.appendChild(frame); + + // Load in separate process if `options.remote` is `true`. + // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsFrameLoader.cpp#1347 + if (remote && !isXUL) { + frame.QueryInterface(Ci.nsIMozBrowserFrame); + frame.createRemoteFrameLoader(null); + } + + // If browser is remote it won't have a `docShell`. + if (!remote) { + let docShell = getDocShell(frame); + docShell.allowAuth = options.allowAuth || false; + docShell.allowJavascript = options.allowJavascript || false; + docShell.allowPlugins = options.allowPlugins || false; + docShell.allowWindowControl = options.allowWindowControl || false; + } + + return frame; +} +exports.create = create; + +function swapFrameLoaders(from, to) { + return from.QueryInterface(Ci.nsIFrameLoaderOwner).swapFrameLoaders(to); +} +exports.swapFrameLoaders = swapFrameLoaders; diff --git a/addon-sdk/source/lib/sdk/fs/path.js b/addon-sdk/source/lib/sdk/fs/path.js new file mode 100644 index 000000000..4474b2b4a --- /dev/null +++ b/addon-sdk/source/lib/sdk/fs/path.js @@ -0,0 +1,500 @@ +// 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. + +// Adapted version of: +// https://github.com/joyent/node/blob/v0.11.3/lib/path.js + +// Shim process global from node. +var process = Object.create(require('../system')); +process.cwd = process.pathFor.bind(process, 'CurProcD'); + +// Update original check in node `process.platform === 'win32'` since in SDK it's `winnt`. +var isWindows = process.platform.indexOf('win') === 0; + + + +// resolves . and .. elements in a path array with directory names there +// must be no slashes, empty elements, or device names (c:\) in the array +// (so also no leading and trailing slashes - it does not distinguish +// relative and absolute paths) +function normalizeArray(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + parts.unshift('..'); + } + } + + return parts; +} + + +if (isWindows) { + // Regex to split a windows path into three parts: [*, device, slash, + // tail] windows-only + var splitDeviceRe = + /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/; + + // Regex to split the tail part of the above into [*, dir, basename, ext] + var splitTailRe = + /^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/; + + // Function to split a filename into [root, dir, basename, ext] + // windows version + var splitPath = function(filename) { + // Separate device+slash from tail + var result = splitDeviceRe.exec(filename), + device = (result[1] || '') + (result[2] || ''), + tail = result[3] || ''; + // Split the tail into dir, basename and extension + var result2 = splitTailRe.exec(tail), + dir = result2[1], + basename = result2[2], + ext = result2[3]; + return [device, dir, basename, ext]; + }; + + var normalizeUNCRoot = function(device) { + return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\'); + }; + + // path.resolve([from ...], to) + // windows version + exports.resolve = function() { + var resolvedDevice = '', + resolvedTail = '', + resolvedAbsolute = false; + + for (var i = arguments.length - 1; i >= -1; i--) { + var path; + if (i >= 0) { + path = arguments[i]; + } else if (!resolvedDevice) { + path = process.cwd(); + } else { + // Windows has the concept of drive-specific current working + // directories. If we've resolved a drive letter but not yet an + // absolute path, get cwd for that drive. We're sure the device is not + // an unc path at this points, because unc paths are always absolute. + path = process.env['=' + resolvedDevice]; + // Verify that a drive-local cwd was found and that it actually points + // to our drive. If not, default to the drive's root. + if (!path || path.substr(0, 3).toLowerCase() !== + resolvedDevice.toLowerCase() + '\\') { + path = resolvedDevice + '\\'; + } + } + + // Skip empty and invalid entries + if (typeof path !== 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + continue; + } + + var result = splitDeviceRe.exec(path), + device = result[1] || '', + isUnc = device && device.charAt(1) !== ':', + isAbsolute = exports.isAbsolute(path), + tail = result[3]; + + if (device && + resolvedDevice && + device.toLowerCase() !== resolvedDevice.toLowerCase()) { + // This path points to another device so it is not applicable + continue; + } + + if (!resolvedDevice) { + resolvedDevice = device; + } + if (!resolvedAbsolute) { + resolvedTail = tail + '\\' + resolvedTail; + resolvedAbsolute = isAbsolute; + } + + if (resolvedDevice && resolvedAbsolute) { + break; + } + } + + // Convert slashes to backslashes when `resolvedDevice` points to an UNC + // root. Also squash multiple slashes into a single one where appropriate. + if (isUnc) { + resolvedDevice = normalizeUNCRoot(resolvedDevice); + } + + // At this point the path should be resolved to a full absolute path, + // but handle relative paths to be safe (might happen when process.cwd() + // fails) + + // Normalize the tail path + + function f(p) { + return !!p; + } + + resolvedTail = normalizeArray(resolvedTail.split(/[\\\/]+/).filter(f), + !resolvedAbsolute).join('\\'); + + return (resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail) || + '.'; + }; + + // windows version + exports.normalize = function(path) { + var result = splitDeviceRe.exec(path), + device = result[1] || '', + isUnc = device && device.charAt(1) !== ':', + isAbsolute = exports.isAbsolute(path), + tail = result[3], + trailingSlash = /[\\\/]$/.test(tail); + + // If device is a drive letter, we'll normalize to lower case. + if (device && device.charAt(1) === ':') { + device = device[0].toLowerCase() + device.substr(1); + } + + // Normalize the tail path + tail = normalizeArray(tail.split(/[\\\/]+/).filter(function(p) { + return !!p; + }), !isAbsolute).join('\\'); + + if (!tail && !isAbsolute) { + tail = '.'; + } + if (tail && trailingSlash) { + tail += '\\'; + } + + // Convert slashes to backslashes when `device` points to an UNC root. + // Also squash multiple slashes into a single one where appropriate. + if (isUnc) { + device = normalizeUNCRoot(device); + } + + return device + (isAbsolute ? '\\' : '') + tail; + }; + + // windows version + exports.isAbsolute = function(path) { + var result = splitDeviceRe.exec(path), + device = result[1] || '', + isUnc = device && device.charAt(1) !== ':'; + // UNC paths are always absolute + return !!result[2] || isUnc; + }; + + // windows version + exports.join = function() { + function f(p) { + if (typeof p !== 'string') { + throw new TypeError('Arguments to path.join must be strings'); + } + return p; + } + + var paths = Array.prototype.filter.call(arguments, f); + var joined = paths.join('\\'); + + // Make sure that the joined path doesn't start with two slashes, because + // normalize() will mistake it for an UNC path then. + // + // This step is skipped when it is very clear that the user actually + // intended to point at an UNC path. This is assumed when the first + // non-empty string arguments starts with exactly two slashes followed by + // at least one more non-slash character. + // + // Note that for normalize() to treat a path as an UNC path it needs to + // have at least 2 components, so we don't filter for that here. + // This means that the user can use join to construct UNC paths from + // a server name and a share name; for example: + // path.join('//server', 'share') -> '\\\\server\\share\') + if (!/^[\\\/]{2}[^\\\/]/.test(paths[0])) { + joined = joined.replace(/^[\\\/]{2,}/, '\\'); + } + + return exports.normalize(joined); + }; + + // path.relative(from, to) + // it will solve the relative path from 'from' to 'to', for instance: + // from = 'C:\\orandea\\test\\aaa' + // to = 'C:\\orandea\\impl\\bbb' + // The output of the function should be: '..\\..\\impl\\bbb' + // windows version + exports.relative = function(from, to) { + from = exports.resolve(from); + to = exports.resolve(to); + + // windows is not case sensitive + var lowerFrom = from.toLowerCase(); + var lowerTo = to.toLowerCase(); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + + var toParts = trim(to.split('\\')); + + var lowerFromParts = trim(lowerFrom.split('\\')); + var lowerToParts = trim(lowerTo.split('\\')); + + var length = Math.min(lowerFromParts.length, lowerToParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (lowerFromParts[i] !== lowerToParts[i]) { + samePartsLength = i; + break; + } + } + + if (samePartsLength == 0) { + return to; + } + + var outputParts = []; + for (var i = samePartsLength; i < lowerFromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('\\'); + }; + + exports.sep = '\\'; + exports.delimiter = ';'; + +} else /* posix */ { + + // Split a filename into [root, dir, basename, ext], unix version + // 'root' is just a slash, or nothing. + var splitPathRe = + /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; + var splitPath = function(filename) { + return splitPathRe.exec(filename).slice(1); + }; + + // path.resolve([from ...], to) + // posix version + exports.resolve = function() { + var resolvedPath = '', + resolvedAbsolute = false; + + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? arguments[i] : process.cwd(); + + // Skip empty and invalid entries + if (typeof path !== 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeArray(resolvedPath.split('/').filter(function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; + }; + + // path.normalize(path) + // posix version + exports.normalize = function(path) { + var isAbsolute = exports.isAbsolute(path), + trailingSlash = path.substr(-1) === '/'; + + // Normalize the path + path = normalizeArray(path.split('/').filter(function(p) { + return !!p; + }), !isAbsolute).join('/'); + + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + + return (isAbsolute ? '/' : '') + path; + }; + + // posix version + exports.isAbsolute = function(path) { + return path.charAt(0) === '/'; + }; + + // posix version + exports.join = function() { + var paths = Array.prototype.slice.call(arguments, 0); + return exports.normalize(paths.filter(function(p, index) { + if (typeof p !== 'string') { + throw new TypeError('Arguments to path.join must be strings'); + } + return p; + }).join('/')); + }; + + + // path.relative(from, to) + // posix version + exports.relative = function(from, to) { + from = exports.resolve(from).substr(1); + to = exports.resolve(to).substr(1); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); + }; + + exports.sep = '/'; + exports.delimiter = ':'; +} + +exports.dirname = function(path) { + var result = splitPath(path), + root = result[0], + dir = result[1]; + + if (!root && !dir) { + // No dirname whatsoever + return '.'; + } + + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.substr(0, dir.length - 1); + } + + return root + dir; +}; + + +exports.basename = function(path, ext) { + var f = splitPath(path)[2]; + // TODO: make this comparison case-insensitive on windows? + if (ext && f.substr(-1 * ext.length) === ext) { + f = f.substr(0, f.length - ext.length); + } + return f; +}; + + +exports.extname = function(path) { + return splitPath(path)[3]; +}; + +if (isWindows) { + exports._makeLong = function(path) { + // Note: this will *probably* throw somewhere. + if (typeof path !== 'string') + return path; + + if (!path) { + return ''; + } + + var resolvedPath = exports.resolve(path); + + if (/^[a-zA-Z]\:\\/.test(resolvedPath)) { + // path is local filesystem path, which needs to be converted + // to long UNC path. + return '\\\\?\\' + resolvedPath; + } else if (/^\\\\[^?.]/.test(resolvedPath)) { + // path is network UNC path, which needs to be converted + // to long UNC path. + return '\\\\?\\UNC\\' + resolvedPath.substring(2); + } + + return path; + }; +} else { + exports._makeLong = function(path) { + return path; + }; +} \ No newline at end of file diff --git a/addon-sdk/source/lib/sdk/hotkeys.js b/addon-sdk/source/lib/sdk/hotkeys.js new file mode 100644 index 000000000..00081455e --- /dev/null +++ b/addon-sdk/source/lib/sdk/hotkeys.js @@ -0,0 +1,40 @@ +/* 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": "stable" +}; + +const INVALID_HOTKEY = "Hotkey must have at least one modifier."; + +const { toJSON: jsonify, toString: stringify, + isFunctionKey } = require("./keyboard/utils"); +const { register, unregister } = require("./keyboard/hotkeys"); + +const Hotkey = exports.Hotkey = function Hotkey(options) { + if (!(this instanceof Hotkey)) + return new Hotkey(options); + + // Parsing key combination string. + let hotkey = jsonify(options.combo); + if (!isFunctionKey(hotkey.key) && !hotkey.modifiers.length) { + throw new TypeError(INVALID_HOTKEY); + } + + this.onPress = options.onPress && options.onPress.bind(this); + this.toString = stringify.bind(null, hotkey); + // Registering listener on keyboard combination enclosed by this hotkey. + // Please note that `this.toString()` is a normalized version of + // `options.combination` where order of modifiers is sorted and `accel` is + // replaced with platform specific key. + register(this.toString(), this.onPress); + // We freeze instance before returning it in order to make it's properties + // read-only. + return Object.freeze(this); +}; +Hotkey.prototype.destroy = function destroy() { + unregister(this.toString(), this.onPress); +}; diff --git a/addon-sdk/source/lib/sdk/indexed-db.js b/addon-sdk/source/lib/sdk/indexed-db.js new file mode 100644 index 000000000..d4d166c02 --- /dev/null +++ b/addon-sdk/source/lib/sdk/indexed-db.js @@ -0,0 +1,79 @@ +/* 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 { Cc, Ci } = require("chrome"); +const { id } = require("./self"); + +// placeholder, copied from bootstrap.js +var sanitizeId = function(id){ + let uuidRe = + /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/; + + let domain = id. + toLowerCase(). + replace(/@/g, "-at-"). + replace(/\./g, "-dot-"). + replace(uuidRe, "$1"); + + return domain +}; + +const PSEUDOURI = "indexeddb://" + sanitizeId(id) // https://bugzilla.mozilla.org/show_bug.cgi?id=779197 + +// Use XPCOM because `require("./url").URL` doesn't expose the raw uri object. +var principaluri = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService). + newURI(PSEUDOURI, null, null); + +var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); +var principal = ssm.createCodebasePrincipal(principaluri, {}); + +function toArray(args) { + return Array.prototype.slice.call(args); +} + +function openInternal(args, forPrincipal, deleting) { + if (forPrincipal) { + args = toArray(args); + } else { + args = [principal].concat(toArray(args)); + } + if (args.length == 2) { + args.push({ storage: "persistent" }); + } else if (!deleting && args.length >= 3 && typeof args[2] === "number") { + args[2] = { version: args[2], storage: "persistent" }; + } + + if (deleting) { + return indexedDB.deleteForPrincipal.apply(indexedDB, args); + } + + return indexedDB.openForPrincipal.apply(indexedDB, args); +} + +exports.indexedDB = Object.freeze({ + open: function () { + return openInternal(arguments, false, false); + }, + deleteDatabase: function () { + return openInternal(arguments, false, true); + }, + openForPrincipal: function () { + return openInternal(arguments, true, false); + }, + deleteForPrincipal: function () { + return openInternal(arguments, true, true); + }, + cmp: indexedDB.cmp.bind(indexedDB) +}); + +exports.IDBKeyRange = IDBKeyRange; +exports.DOMException = Ci.nsIDOMDOMException; diff --git a/addon-sdk/source/lib/sdk/input/browser.js b/addon-sdk/source/lib/sdk/input/browser.js new file mode 100644 index 000000000..daea875bf --- /dev/null +++ b/addon-sdk/source/lib/sdk/input/browser.js @@ -0,0 +1,73 @@ +/* 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 { windows, isBrowser, isInteractive, isDocumentLoaded, + getOuterId } = require("../window/utils"); +const { InputPort } = require("./system"); +const { lift, merges, foldp, keepIf, start, Input } = require("../event/utils"); +const { patch } = require("diffpatcher/index"); +const { Sequence, seq, filter, object, pairs } = require("../util/sequence"); + + +// Create lazy iterators from the regular arrays, although +// once https://github.com/mozilla/addon-sdk/pull/1314 lands +// `windows` will be transforme to lazy iterators. +// When iterated over belowe sequences items will represent +// state of windows at the time of iteration. +const opened = seq(function*() { + const items = windows("navigator:browser", {includePrivate: true}); + for (let item of items) { + yield [getOuterId(item), item]; + } +}); +const interactive = filter(([_, window]) => isInteractive(window), opened); +const loaded = filter(([_, window]) => isDocumentLoaded(window), opened); + +// Helper function that converts given argument to a delta. +const Update = window => window && object([getOuterId(window), window]); +const Delete = window => window && object([getOuterId(window), null]); + + +// Signal represents delta for last top level window close. +const LastClosed = lift(Delete, + keepIf(isBrowser, null, + new InputPort({topic: "domwindowclosed"}))); +exports.LastClosed = LastClosed; + +const windowFor = document => document && document.defaultView; + +// Signal represent delta for last top level window document becoming interactive. +const InteractiveDoc = new InputPort({topic: "chrome-document-interactive"}); +const InteractiveWin = lift(windowFor, InteractiveDoc); +const LastInteractive = lift(Update, keepIf(isBrowser, null, InteractiveWin)); +exports.LastInteractive = LastInteractive; + +// Signal represent delta for last top level window loaded. +const LoadedDoc = new InputPort({topic: "chrome-document-loaded"}); +const LoadedWin = lift(windowFor, LoadedDoc); +const LastLoaded = lift(Update, keepIf(isBrowser, null, LoadedWin)); +exports.LastLoaded = LastLoaded; + + +const initialize = input => { + if (!input.initialized) { + input.value = object(...input.value); + Input.start(input); + input.initialized = true; + } +}; + +// Signal represents set of top level interactive windows, updated any +// time new window becomes interactive or one get's closed. +const Interactive = foldp(patch, interactive, merges([LastInteractive, + LastClosed])); +Interactive[start] = initialize; +exports.Interactive = Interactive; + +// Signal represents set of top level loaded window, updated any time +// new window becomes interactive or one get's closed. +const Loaded = foldp(patch, loaded, merges([LastLoaded, LastClosed])); +Loaded[start] = initialize; +exports.Loaded = Loaded; diff --git a/addon-sdk/source/lib/sdk/input/customizable-ui.js b/addon-sdk/source/lib/sdk/input/customizable-ui.js new file mode 100644 index 000000000..a41d0971a --- /dev/null +++ b/addon-sdk/source/lib/sdk/input/customizable-ui.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"; + +const { Cu } = require("chrome"); +const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {}); +const { receive } = require("../event/utils"); +const { InputPort } = require("./system"); +const { object} = require("../util/sequence"); +const { getOuterId } = require("../window/utils"); + +const Input = function() {}; +Input.prototype = Object.create(InputPort.prototype); + +Input.prototype.onCustomizeStart = function (window) { + receive(this, object([getOuterId(window), true])); +} + +Input.prototype.onCustomizeEnd = function (window) { + receive(this, object([getOuterId(window), null])); +} + +Input.prototype.addListener = input => CustomizableUI.addListener(input); + +Input.prototype.removeListener = input => CustomizableUI.removeListener(input); + +exports.CustomizationInput = Input; diff --git a/addon-sdk/source/lib/sdk/input/frame.js b/addon-sdk/source/lib/sdk/input/frame.js new file mode 100644 index 000000000..50efaa745 --- /dev/null +++ b/addon-sdk/source/lib/sdk/input/frame.js @@ -0,0 +1,85 @@ +/* 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 { Ci } = require("chrome"); +const { InputPort } = require("./system"); +const { getFrameElement, getOuterId, + getOwnerBrowserWindow } = require("../window/utils"); +const { isnt } = require("../lang/functional"); +const { foldp, lift, merges, keepIf } = require("../event/utils"); +const { object } = require("../util/sequence"); +const { compose } = require("../lang/functional"); +const { LastClosed } = require("./browser"); +const { patch } = require("diffpatcher/index"); + +const Document = Ci.nsIDOMDocument; + +const isntNull = isnt(null); + +const frameID = frame => frame.id; +const browserID = compose(getOuterId, getOwnerBrowserWindow); + +const isInnerFrame = frame => + frame && frame.hasAttribute("data-is-sdk-inner-frame"); + +// Utility function that given content window loaded in our frame views returns +// an actual frame. This basically takes care of fact that actual frame document +// is loaded in the nested iframe. If content window is not loaded in the nested +// frame of the frame view it returs null. +const getFrame = document => + document && document.defaultView && getFrameElement(document.defaultView); + +const FrameInput = function(options) { + const input = keepIf(isInnerFrame, null, + lift(getFrame, new InputPort(options))); + return lift(frame => { + if (!frame) return frame; + const [id, owner] = [frameID(frame), browserID(frame)]; + return object([id, {owners: object([owner, options.update])}]); + }, input); +}; + +const LastLoading = new FrameInput({topic: "document-element-inserted", + update: {readyState: "loading"}}); +exports.LastLoading = LastLoading; + +const LastInteractive = new FrameInput({topic: "content-document-interactive", + update: {readyState: "interactive"}}); +exports.LastInteractive = LastInteractive; + +const LastLoaded = new FrameInput({topic: "content-document-loaded", + update: {readyState: "complete"}}); +exports.LastLoaded = LastLoaded; + +const LastUnloaded = new FrameInput({topic: "content-page-hidden", + update: null}); +exports.LastUnloaded = LastUnloaded; + +// Represents state of SDK frames in form of data structure: +// {"frame#1": {"id": "frame#1", +// "inbox": {"data": "ping", +// "target": {"id": "frame#1", "owner": "outerWindowID#2"}, +// "source": {"id": "frame#1"}} +// "url": "resource://addon-1/data/index.html", +// "owners": {"outerWindowID#1": {"readyState": "loading"}, +// "outerWindowID#2": {"readyState": "complete"}} +// +// +// frame#2: {"id": "frame#2", +// "url": "resource://addon-1/data/main.html", +// "outbox": {"data": "pong", +// "source": {"id": "frame#2", "owner": "outerWindowID#1"} +// "target": {"id": "frame#2"}} +// "owners": {outerWindowID#1: {readyState: "interacitve"}}}} +const Frames = foldp(patch, {}, merges([ + LastLoading, + LastInteractive, + LastLoaded, + LastUnloaded, + new InputPort({ id: "frame-mailbox" }), + new InputPort({ id: "frame-change" }), + new InputPort({ id: "frame-changed" }) +])); +exports.Frames = Frames; diff --git a/addon-sdk/source/lib/sdk/input/system.js b/addon-sdk/source/lib/sdk/input/system.js new file mode 100644 index 000000000..66bc6daec --- /dev/null +++ b/addon-sdk/source/lib/sdk/input/system.js @@ -0,0 +1,113 @@ +/* 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, Ci, Cr, Cu } = require("chrome"); +const { Input, start, stop, end, receive, outputs } = require("../event/utils"); +const { once, off } = require("../event/core"); +const { id: addonID } = require("../self"); + +const unloadMessage = require("@loader/unload"); +const observerService = Cc['@mozilla.org/observer-service;1']. + getService(Ci.nsIObserverService); +const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm"); +const addObserver = ShimWaiver.getProperty(observerService, "addObserver"); +const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver"); + + +const addonUnloadTopic = "sdk:loader:destroy"; + +const isXrayWrapper = Cu.isXrayWrapper; +// In the past SDK used to double-wrap notifications dispatched, which +// made them awkward to use outside of SDK. At present they no longer +// do that, although we still supported for legacy reasons. +const isLegacyWrapper = x => + x && x.wrappedJSObject && + "observersModuleSubjectWrapper" in x.wrappedJSObject; + +const unwrapLegacy = x => x.wrappedJSObject.object; + +// `InputPort` provides a way to create a signal out of the observer +// notification subject's for the given `topic`. If `options.initial` +// is provided it is used as initial value otherwise `null` is used. +// Constructor can be given `options.id` that will be used to create +// a `topic` which is namespaced to an add-on (this avoids conflicts +// when multiple add-on are used, although in a future host probably +// should just be shared across add-ons). It is also possible to +// specify a specific `topic` via `options.topic` which is used as +// without namespacing. Created signal ends whenever add-on is +// unloaded. +const InputPort = function InputPort({id, topic, initial}) { + this.id = id || topic; + this.topic = topic || "sdk:" + addonID + ":" + id; + this.value = initial === void(0) ? null : initial; + this.observing = false; + this[outputs] = []; +}; + +// InputPort type implements `Input` signal interface. +InputPort.prototype = new Input(); +InputPort.prototype.constructor = InputPort; + +// When port is started (which is when it's subgraph get's +// first subscriber) actual observer is registered. +InputPort.start = input => { + input.addListener(input); + // Also register add-on unload observer to end this signal + // when that happens. + addObserver(input, addonUnloadTopic, false); +}; +InputPort.prototype[start] = InputPort.start; + +InputPort.addListener = input => addObserver(input, input.topic, false); +InputPort.prototype.addListener = InputPort.addListener; + +// When port is stopped (which is when it's subgraph has no +// no subcribers left) an actual observer unregistered. +// Note that port stopped once it ends as well (which is when +// add-on is unloaded). +InputPort.stop = input => { + input.removeListener(input); + removeObserver(input, addonUnloadTopic); +}; +InputPort.prototype[stop] = InputPort.stop; + +InputPort.removeListener = input => removeObserver(input, input.topic); +InputPort.prototype.removeListener = InputPort.removeListener; + +// `InputPort` also implements `nsIObserver` interface and +// `nsISupportsWeakReference` interfaces as it's going to be used as such. +InputPort.prototype.QueryInterface = function(iid) { + if (!iid.equals(Ci.nsIObserver) && !iid.equals(Ci.nsISupportsWeakReference)) + throw Cr.NS_ERROR_NO_INTERFACE; + + return this; +}; + +// `InputPort` instances implement `observe` method, which is invoked when +// observer notifications are dispatched. The `subject` of that notification +// are received on this signal. +InputPort.prototype.observe = function(subject, topic, data) { + // Unwrap message from the subject. SDK used to have it's own version of + // wrappedJSObjects which take precedence, if subject has `wrappedJSObject` + // and it's not an XrayWrapper use it as message. Otherwise use subject as + // is. + const message = subject === null ? null : + isLegacyWrapper(subject) ? unwrapLegacy(subject) : + isXrayWrapper(subject) ? subject : + subject.wrappedJSObject ? subject.wrappedJSObject : + subject; + + // If observer topic matches topic of the input port receive a message. + if (topic === this.topic) { + receive(this, message); + } + + // If observe topic is add-on unload topic we create an end message. + if (topic === addonUnloadTopic && message === unloadMessage) { + end(this); + } +}; + +exports.InputPort = InputPort; diff --git a/addon-sdk/source/lib/sdk/io/buffer.js b/addon-sdk/source/lib/sdk/io/buffer.js new file mode 100644 index 000000000..5ea169402 --- /dev/null +++ b/addon-sdk/source/lib/sdk/io/buffer.js @@ -0,0 +1,351 @@ +/* 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' +}; + +/* + * Encodings supported by TextEncoder/Decoder: + * utf-8, utf-16le, utf-16be + * http://encoding.spec.whatwg.org/#interface-textencoder + * + * Node however supports the following encodings: + * ascii, utf-8, utf-16le, usc2, base64, hex + */ + +const { Cu } = require('chrome'); +const { isNumber } = require('sdk/lang/type'); +const { TextEncoder, TextDecoder } = Cu.import('resource://gre/modules/commonjs/toolkit/loader.js', {}); + +exports.TextEncoder = TextEncoder; +exports.TextDecoder = TextDecoder; + +/** + * Use WeakMaps to work around Bug 929146, which prevents us from adding + * getters or values to typed arrays + * https://bugzilla.mozilla.org/show_bug.cgi?id=929146 + */ +const parents = new WeakMap(); +const views = new WeakMap(); + +function Buffer(subject, encoding /*, bufferLength */) { + + // Allow invocation without `new` constructor + if (!(this instanceof Buffer)) + return new Buffer(subject, encoding, arguments[2]); + + var type = typeof(subject); + + switch (type) { + case 'number': + // Create typed array of the given size if number. + try { + let buffer = new Uint8Array(subject > 0 ? Math.floor(subject) : 0); + return buffer; + } catch (e) { + if (/size and count too large/.test(e.message) || + /invalid arguments/.test(e.message)) + throw new RangeError('Could not instantiate buffer: size of buffer may be too large'); + else + throw new Error('Could not instantiate buffer'); + } + break; + case 'string': + // If string encode it and use buffer for the returned Uint8Array + // to create a local patched version that acts like node buffer. + encoding = encoding || 'utf8'; + return new Uint8Array(new TextEncoder(encoding).encode(subject).buffer); + case 'object': + // This form of the constructor uses the form of + // new Uint8Array(buffer, offset, length); + // So we can instantiate a typed array within the constructor + // to inherit the appropriate properties, where both the + // `subject` and newly instantiated buffer share the same underlying + // data structure. + if (arguments.length === 3) + return new Uint8Array(subject, encoding, arguments[2]); + // If array or alike just make a copy with a local patched prototype. + else + return new Uint8Array(subject); + default: + throw new TypeError('must start with number, buffer, array or string'); + } +} +exports.Buffer = Buffer; + +// Tests if `value` is a Buffer. +Buffer.isBuffer = value => value instanceof Buffer + +// Returns true if the encoding is a valid encoding argument & false otherwise +Buffer.isEncoding = function (encoding) { + if (!encoding) return false; + try { + new TextDecoder(encoding); + } catch(e) { + return false; + } + return true; +} + +// Gives the actual byte length of a string. encoding defaults to 'utf8'. +// This is not the same as String.prototype.length since that returns the +// number of characters in a string. +Buffer.byteLength = (value, encoding = 'utf8') => + new TextEncoder(encoding).encode(value).byteLength + +// Direct copy of the nodejs's buffer implementation: +// https://github.com/joyent/node/blob/b255f4c10a80343f9ce1cee56d0288361429e214/lib/buffer.js#L146-L177 +Buffer.concat = function(list, length) { + if (!Array.isArray(list)) + throw new TypeError('Usage: Buffer.concat(list[, length])'); + + if (typeof length === 'undefined') { + length = 0; + for (var i = 0; i < list.length; i++) + length += list[i].length; + } else { + length = ~~length; + } + + if (length < 0) + length = 0; + + if (list.length === 0) + return new Buffer(0); + else if (list.length === 1) + return list[0]; + + if (length < 0) + throw new RangeError('length is not a positive number'); + + var buffer = new Buffer(length); + var pos = 0; + for (var i = 0; i < list.length; i++) { + var buf = list[i]; + buf.copy(buffer, pos); + pos += buf.length; + } + + return buffer; +}; + +// Node buffer is very much like Uint8Array although it has bunch of methods +// that typically can be used in combination with `DataView` while preserving +// access by index. Since in SDK each module has it's own set of bult-ins it +// ok to patch ours to make it nodejs Buffer compatible. +const Uint8ArraySet = Uint8Array.prototype.set +Buffer.prototype = Uint8Array.prototype; +Object.defineProperties(Buffer.prototype, { + parent: { + get: function() { return parents.get(this, undefined); } + }, + view: { + get: function () { + let view = views.get(this, undefined); + if (view) return view; + view = new DataView(this.buffer); + views.set(this, view); + return view; + } + }, + toString: { + value: function(encoding, start, end) { + encoding = !!encoding ? (encoding + '').toLowerCase() : 'utf8'; + start = Math.max(0, ~~start); + end = Math.min(this.length, end === void(0) ? this.length : ~~end); + return new TextDecoder(encoding).decode(this.subarray(start, end)); + } + }, + toJSON: { + value: function() { + return { type: 'Buffer', data: Array.slice(this, 0) }; + } + }, + get: { + value: function(offset) { + return this[offset]; + } + }, + set: { + value: function(offset, value) { this[offset] = value; } + }, + copy: { + value: function(target, offset, start, end) { + let length = this.length; + let targetLength = target.length; + offset = isNumber(offset) ? offset : 0; + start = isNumber(start) ? start : 0; + + if (start < 0) + throw new RangeError('sourceStart is outside of valid range'); + if (end < 0) + throw new RangeError('sourceEnd is outside of valid range'); + + // If sourceStart > sourceEnd, or targetStart > targetLength, + // zero bytes copied + if (start > end || + offset > targetLength + ) + return 0; + + // If `end` is not defined, or if it is defined + // but would overflow `target`, redefine `end` + // so we can copy as much as we can + if (end - start > targetLength - offset || + end == null) { + let remainingTarget = targetLength - offset; + let remainingSource = length - start; + if (remainingSource <= remainingTarget) + end = length; + else + end = start + remainingTarget; + } + + Uint8ArraySet.call(target, this.subarray(start, end), offset); + return end - start; + } + }, + slice: { + value: function(start, end) { + let length = this.length; + start = ~~start; + end = end != null ? end : length; + + if (start < 0) { + start += length; + if (start < 0) start = 0; + } else if (start > length) + start = length; + + if (end < 0) { + end += length; + if (end < 0) end = 0; + } else if (end > length) + end = length; + + if (end < start) + end = start; + + // This instantiation uses the new Uint8Array(buffer, offset, length) version + // of construction to share the same underling data structure + let buffer = new Buffer(this.buffer, start, end - start); + + // If buffer has a value, assign its parent value to the + // buffer it shares its underlying structure with. If a slice of + // a slice, then use the root structure + if (buffer.length > 0) + parents.set(buffer, this.parent || this); + + return buffer; + } + }, + write: { + value: function(string, offset, length, encoding = 'utf8') { + // write(string, encoding); + if (typeof(offset) === 'string' && Number.isNaN(parseInt(offset))) { + [offset, length, encoding] = [0, null, offset]; + } + // write(string, offset, encoding); + else if (typeof(length) === 'string') + [length, encoding] = [null, length]; + + if (offset < 0 || offset > this.length) + throw new RangeError('offset is outside of valid range'); + + offset = ~~offset; + + // Clamp length if it would overflow buffer, or if its + // undefined + if (length == null || length + offset > this.length) + length = this.length - offset; + + let buffer = new TextEncoder(encoding).encode(string); + let result = Math.min(buffer.length, length); + if (buffer.length !== length) + buffer = buffer.subarray(0, length); + + Uint8ArraySet.call(this, buffer, offset); + return result; + } + }, + fill: { + value: function fill(value, start, end) { + let length = this.length; + value = value || 0; + start = start || 0; + end = end || length; + + if (typeof(value) === 'string') + value = value.charCodeAt(0); + if (typeof(value) !== 'number' || isNaN(value)) + throw TypeError('value is not a number'); + if (end < start) + throw new RangeError('end < start'); + + // Fill 0 bytes; we're done + if (end === start) + return 0; + if (length == 0) + return 0; + + if (start < 0 || start >= length) + throw RangeError('start out of bounds'); + + if (end < 0 || end > length) + throw RangeError('end out of bounds'); + + let index = start; + while (index < end) this[index++] = value; + } + } +}); + +// Define nodejs Buffer's getter and setter functions that just proxy +// to internal DataView's equivalent methods. + +// TODO do we need to check architecture to see if it's default big/little endian? +[['readUInt16LE', 'getUint16', true], + ['readUInt16BE', 'getUint16', false], + ['readInt16LE', 'getInt16', true], + ['readInt16BE', 'getInt16', false], + ['readUInt32LE', 'getUint32', true], + ['readUInt32BE', 'getUint32', false], + ['readInt32LE', 'getInt32', true], + ['readInt32BE', 'getInt32', false], + ['readFloatLE', 'getFloat32', true], + ['readFloatBE', 'getFloat32', false], + ['readDoubleLE', 'getFloat64', true], + ['readDoubleBE', 'getFloat64', false], + ['readUInt8', 'getUint8'], + ['readInt8', 'getInt8']].forEach(([alias, name, littleEndian]) => { + Object.defineProperty(Buffer.prototype, alias, { + value: function(offset) { + return this.view[name](offset, littleEndian); + } + }); +}); + +[['writeUInt16LE', 'setUint16', true], + ['writeUInt16BE', 'setUint16', false], + ['writeInt16LE', 'setInt16', true], + ['writeInt16BE', 'setInt16', false], + ['writeUInt32LE', 'setUint32', true], + ['writeUInt32BE', 'setUint32', false], + ['writeInt32LE', 'setInt32', true], + ['writeInt32BE', 'setInt32', false], + ['writeFloatLE', 'setFloat32', true], + ['writeFloatBE', 'setFloat32', false], + ['writeDoubleLE', 'setFloat64', true], + ['writeDoubleBE', 'setFloat64', false], + ['writeUInt8', 'setUint8'], + ['writeInt8', 'setInt8']].forEach(([alias, name, littleEndian]) => { + Object.defineProperty(Buffer.prototype, alias, { + value: function(value, offset) { + return this.view[name](offset, value, littleEndian); + } + }); +}); diff --git a/addon-sdk/source/lib/sdk/io/byte-streams.js b/addon-sdk/source/lib/sdk/io/byte-streams.js new file mode 100644 index 000000000..6afab4369 --- /dev/null +++ b/addon-sdk/source/lib/sdk/io/byte-streams.js @@ -0,0 +1,104 @@ +/* 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" +}; + +exports.ByteReader = ByteReader; +exports.ByteWriter = ByteWriter; + +const {Cc, Ci} = require("chrome"); + +// This just controls the maximum number of bytes we read in at one time. +const BUFFER_BYTE_LEN = 0x8000; + +function ByteReader(inputStream) { + const self = this; + + let stream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(inputStream); + + let manager = new StreamManager(this, stream); + + this.read = function ByteReader_read(numBytes) { + manager.ensureOpened(); + if (typeof(numBytes) !== "number") + numBytes = Infinity; + + let data = ""; + let read = 0; + try { + while (true) { + let avail = stream.available(); + let toRead = Math.min(numBytes - read, avail, BUFFER_BYTE_LEN); + if (toRead <= 0) + break; + data += stream.readBytes(toRead); + read += toRead; + } + } + catch (err) { + throw new Error("Error reading from stream: " + err); + } + + return data; + }; +} + +function ByteWriter(outputStream) { + const self = this; + + let stream = Cc["@mozilla.org/binaryoutputstream;1"]. + createInstance(Ci.nsIBinaryOutputStream); + stream.setOutputStream(outputStream); + + let manager = new StreamManager(this, stream); + + this.write = function ByteWriter_write(str) { + manager.ensureOpened(); + try { + stream.writeBytes(str, str.length); + } + catch (err) { + throw new Error("Error writing to stream: " + err); + } + }; +} + + +// This manages the lifetime of stream, a ByteReader or ByteWriter. It defines +// closed and close() on stream and registers an unload listener that closes +// rawStream if it's still opened. It also provides ensureOpened(), which +// throws an exception if the stream is closed. +function StreamManager(stream, rawStream) { + const self = this; + this.rawStream = rawStream; + this.opened = true; + + stream.__defineGetter__("closed", function stream_closed() { + return !self.opened; + }); + + stream.close = function stream_close() { + self.ensureOpened(); + self.unload(); + }; + + require("../system/unload").ensure(this); +} + +StreamManager.prototype = { + ensureOpened: function StreamManager_ensureOpened() { + if (!this.opened) + throw new Error("The stream is closed and cannot be used."); + }, + unload: function StreamManager_unload() { + this.rawStream.close(); + this.opened = false; + } +}; diff --git a/addon-sdk/source/lib/sdk/io/file.js b/addon-sdk/source/lib/sdk/io/file.js new file mode 100644 index 000000000..47467df87 --- /dev/null +++ b/addon-sdk/source/lib/sdk/io/file.js @@ -0,0 +1,196 @@ +/* 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": "deprecated" +}; + +const {Cc,Ci,Cr} = require("chrome"); +const byteStreams = require("./byte-streams"); +const textStreams = require("./text-streams"); + +// Flags passed when opening a file. See nsprpub/pr/include/prio.h. +const OPEN_FLAGS = { + RDONLY: parseInt("0x01"), + WRONLY: parseInt("0x02"), + CREATE_FILE: parseInt("0x08"), + APPEND: parseInt("0x10"), + TRUNCATE: parseInt("0x20"), + EXCL: parseInt("0x80") +}; + +var dirsvc = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + +function MozFile(path) { + var file = Cc['@mozilla.org/file/local;1'] + .createInstance(Ci.nsILocalFile); + file.initWithPath(path); + return file; +} + +function ensureReadable(file) { + if (!file.isReadable()) + throw new Error("path is not readable: " + file.path); +} + +function ensureDir(file) { + ensureExists(file); + if (!file.isDirectory()) + throw new Error("path is not a directory: " + file.path); +} + +function ensureFile(file) { + ensureExists(file); + if (!file.isFile()) + throw new Error("path is not a file: " + file.path); +} + +function ensureExists(file) { + if (!file.exists()) + throw friendlyError(Cr.NS_ERROR_FILE_NOT_FOUND, file.path); +} + +function friendlyError(errOrResult, filename) { + var isResult = typeof(errOrResult) === "number"; + var result = isResult ? errOrResult : errOrResult.result; + switch (result) { + case Cr.NS_ERROR_FILE_NOT_FOUND: + return new Error("path does not exist: " + filename); + } + return isResult ? new Error("XPCOM error code: " + errOrResult) : errOrResult; +} + +exports.exists = function exists(filename) { + return MozFile(filename).exists(); +}; + +exports.isFile = function isFile(filename) { + return MozFile(filename).isFile(); +}; + +exports.read = function read(filename, mode) { + if (typeof(mode) !== "string") + mode = ""; + + // Ensure mode is read-only. + mode = /b/.test(mode) ? "b" : ""; + + var stream = exports.open(filename, mode); + try { + var str = stream.read(); + } + finally { + stream.close(); + } + + return str; +}; + +exports.join = function join(base) { + if (arguments.length < 2) + throw new Error("need at least 2 args"); + base = MozFile(base); + for (var i = 1; i < arguments.length; i++) + base.append(arguments[i]); + return base.path; +}; + +exports.dirname = function dirname(path) { + var parent = MozFile(path).parent; + return parent ? parent.path : ""; +}; + +exports.basename = function basename(path) { + var leafName = MozFile(path).leafName; + + // On Windows, leafName when the path is a volume letter and colon ("c:") is + // the path itself. But such a path has no basename, so we want the empty + // string. + return leafName == path ? "" : leafName; +}; + +exports.list = function list(path) { + var file = MozFile(path); + ensureDir(file); + ensureReadable(file); + + var entries = file.directoryEntries; + var entryNames = []; + while(entries.hasMoreElements()) { + var entry = entries.getNext(); + entry.QueryInterface(Ci.nsIFile); + entryNames.push(entry.leafName); + } + return entryNames; +}; + +exports.open = function open(filename, mode) { + var file = MozFile(filename); + if (typeof(mode) !== "string") + mode = ""; + + // File opened for write only. + if (/w/.test(mode)) { + if (file.exists()) + ensureFile(file); + var stream = Cc['@mozilla.org/network/file-output-stream;1']. + createInstance(Ci.nsIFileOutputStream); + var openFlags = OPEN_FLAGS.WRONLY | + OPEN_FLAGS.CREATE_FILE | + OPEN_FLAGS.TRUNCATE; + var permFlags = 0o644; // u+rw go+r + try { + stream.init(file, openFlags, permFlags, 0); + } + catch (err) { + throw friendlyError(err, filename); + } + return /b/.test(mode) ? + new byteStreams.ByteWriter(stream) : + new textStreams.TextWriter(stream); + } + + // File opened for read only, the default. + ensureFile(file); + stream = Cc['@mozilla.org/network/file-input-stream;1']. + createInstance(Ci.nsIFileInputStream); + try { + stream.init(file, OPEN_FLAGS.RDONLY, 0, 0); + } + catch (err) { + throw friendlyError(err, filename); + } + return /b/.test(mode) ? + new byteStreams.ByteReader(stream) : + new textStreams.TextReader(stream); +}; + +exports.remove = function remove(path) { + var file = MozFile(path); + ensureFile(file); + file.remove(false); +}; + +exports.mkpath = function mkpath(path) { + var file = MozFile(path); + if (!file.exists()) + file.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); // u+rwx go+rx + else if (!file.isDirectory()) + throw new Error("The path already exists and is not a directory: " + path); +}; + +exports.rmdir = function rmdir(path) { + var file = MozFile(path); + ensureDir(file); + try { + file.remove(false); + } + catch (err) { + // Bug 566950 explains why we're not catching a specific exception here. + throw new Error("The directory is not empty: " + path); + } +}; diff --git a/addon-sdk/source/lib/sdk/io/fs.js b/addon-sdk/source/lib/sdk/io/fs.js new file mode 100644 index 000000000..860a884a5 --- /dev/null +++ b/addon-sdk/source/lib/sdk/io/fs.js @@ -0,0 +1,984 @@ +/* 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 { Cc, Ci, CC } = require("chrome"); + +const { setTimeout } = require("../timers"); +const { Stream, InputStream, OutputStream } = require("./stream"); +const { emit, on } = require("../event/core"); +const { Buffer } = require("./buffer"); +const { ns } = require("../core/namespace"); +const { Class } = require("../core/heritage"); + + +const nsILocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile", + "initWithPath"); +const FileOutputStream = CC("@mozilla.org/network/file-output-stream;1", + "nsIFileOutputStream", "init"); +const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", "init"); +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", "setInputStream"); +const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", "setOutputStream"); +const StreamPump = CC("@mozilla.org/network/input-stream-pump;1", + "nsIInputStreamPump", "init"); + +const { createOutputTransport, createInputTransport } = + Cc["@mozilla.org/network/stream-transport-service;1"]. + getService(Ci.nsIStreamTransportService); + +const { OPEN_UNBUFFERED } = Ci.nsITransport; + + +const { REOPEN_ON_REWIND, DEFER_OPEN } = Ci.nsIFileInputStream; +const { DIRECTORY_TYPE, NORMAL_FILE_TYPE } = Ci.nsIFile; +const { NS_SEEK_SET, NS_SEEK_CUR, NS_SEEK_END } = Ci.nsISeekableStream; + +const FILE_PERMISSION = 0o666; +const PR_UINT32_MAX = 0xfffffff; +// Values taken from: +// http://mxr.mozilla.org/mozilla-central/source/nsprpub/pr/include/prio.h#615 +const PR_RDONLY = 0x01; +const PR_WRONLY = 0x02; +const PR_RDWR = 0x04; +const PR_CREATE_FILE = 0x08; +const PR_APPEND = 0x10; +const PR_TRUNCATE = 0x20; +const PR_SYNC = 0x40; +const PR_EXCL = 0x80; + +const FLAGS = { + "r": PR_RDONLY, + "r+": PR_RDWR, + "w": PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY, + "w+": PR_CREATE_FILE | PR_TRUNCATE | PR_RDWR, + "a": PR_APPEND | PR_CREATE_FILE | PR_WRONLY, + "a+": PR_APPEND | PR_CREATE_FILE | PR_RDWR +}; + +function accessor() { + let map = new WeakMap(); + return function(fd, value) { + if (value === null) map.delete(fd); + if (value !== undefined) map.set(fd, value); + return map.get(fd); + } +} + +var nsIFile = accessor(); +var nsIFileInputStream = accessor(); +var nsIFileOutputStream = accessor(); +var nsIBinaryInputStream = accessor(); +var nsIBinaryOutputStream = accessor(); + +// Just a contstant object used to signal that all of the file +// needs to be read. +const ALL = new String("Read all of the file"); + +function isWritable(mode) { + return !!(mode & PR_WRONLY || mode & PR_RDWR); +} +function isReadable(mode) { + return !!(mode & PR_RDONLY || mode & PR_RDWR); +} + +function isString(value) { + return typeof(value) === "string"; +} +function isFunction(value) { + return typeof(value) === "function"; +} + +function toArray(enumerator) { + let value = []; + while(enumerator.hasMoreElements()) + value.push(enumerator.getNext()) + return value +} + +function getFileName(file) { + return file.QueryInterface(Ci.nsIFile).leafName; +} + + +function remove(path, recursive) { + let fd = new nsILocalFile(path) + if (fd.exists()) { + fd.remove(recursive || false); + } + else { + throw FSError("remove", "ENOENT", 34, path); + } +} + +/** + * Utility function to convert either an octal number or string + * into an octal number + * 0777 => 0o777 + * "0644" => 0o644 + */ +function Mode(mode, fallback) { + return isString(mode) ? parseInt(mode, 8) : mode || fallback; +} +function Flags(flag) { + return !isString(flag) ? flag : + FLAGS[flag] || Error("Unknown file open flag: " + flag); +} + + +function FSError(op, code, errno, path, file, line) { + let error = Error(code + ", " + op + " " + path, file, line); + error.code = code; + error.path = path; + error.errno = errno; + return error; +} + +const ReadStream = Class({ + extends: InputStream, + initialize: function initialize(path, options) { + this.position = -1; + this.length = -1; + this.flags = "r"; + this.mode = FILE_PERMISSION; + this.bufferSize = 64 * 1024; + + options = options || {}; + + if ("flags" in options && options.flags) + this.flags = options.flags; + if ("bufferSize" in options && options.bufferSize) + this.bufferSize = options.bufferSize; + if ("length" in options && options.length) + this.length = options.length; + if ("position" in options && options.position !== undefined) + this.position = options.position; + + let { flags, mode, position, length } = this; + let fd = isString(path) ? openSync(path, flags, mode) : path; + this.fd = fd; + + let input = nsIFileInputStream(fd); + // Setting a stream position, unless it"s `-1` which means current position. + if (position >= 0) + input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position); + // We use `nsIStreamTransportService` service to transform blocking + // file input stream into a fully asynchronous stream that can be written + // without blocking the main thread. + let transport = createInputTransport(input, position, length, false); + // Open an input stream on a transport. We don"t pass flags to guarantee + // non-blocking stream semantics. Also we use defaults for segment size & + // count. + InputStream.prototype.initialize.call(this, { + asyncInputStream: transport.openInputStream(null, 0, 0) + }); + + // Close file descriptor on end and destroy the stream. + on(this, "end", _ => { + this.destroy(); + emit(this, "close"); + }); + + this.read(); + }, + destroy: function() { + closeSync(this.fd); + InputStream.prototype.destroy.call(this); + } +}); +exports.ReadStream = ReadStream; +exports.createReadStream = function createReadStream(path, options) { + return new ReadStream(path, options); +}; + +const WriteStream = Class({ + extends: OutputStream, + initialize: function initialize(path, options) { + this.drainable = true; + this.flags = "w"; + this.position = -1; + this.mode = FILE_PERMISSION; + + options = options || {}; + + if ("flags" in options && options.flags) + this.flags = options.flags; + if ("mode" in options && options.mode) + this.mode = options.mode; + if ("position" in options && options.position !== undefined) + this.position = options.position; + + let { position, flags, mode } = this; + // If pass was passed we create a file descriptor out of it. Otherwise + // we just use given file descriptor. + let fd = isString(path) ? openSync(path, flags, mode) : path; + this.fd = fd; + + let output = nsIFileOutputStream(fd); + // Setting a stream position, unless it"s `-1` which means current position. + if (position >= 0) + output.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position); + // We use `nsIStreamTransportService` service to transform blocking + // file output stream into a fully asynchronous stream that can be written + // without blocking the main thread. + let transport = createOutputTransport(output, position, -1, false); + // Open an output stream on a transport. We don"t pass flags to guarantee + // non-blocking stream semantics. Also we use defaults for segment size & + // count. + OutputStream.prototype.initialize.call(this, { + asyncOutputStream: transport.openOutputStream(OPEN_UNBUFFERED, 0, 0), + output: output + }); + + // For write streams "finish" basically means close. + on(this, "finish", _ => { + this.destroy(); + emit(this, "close"); + }); + }, + destroy: function() { + OutputStream.prototype.destroy.call(this); + closeSync(this.fd); + } +}); +exports.WriteStream = WriteStream; +exports.createWriteStream = function createWriteStream(path, options) { + return new WriteStream(path, options); +}; + +const Stats = Class({ + initialize: function initialize(path) { + let file = new nsILocalFile(path); + if (!file.exists()) throw FSError("stat", "ENOENT", 34, path); + nsIFile(this, file); + }, + isDirectory: function() { + return nsIFile(this).isDirectory(); + }, + isFile: function() { + return nsIFile(this).isFile(); + }, + isSymbolicLink: function() { + return nsIFile(this).isSymlink(); + }, + get mode() { + return nsIFile(this).permissions; + }, + get size() { + return nsIFile(this).fileSize; + }, + get mtime() { + return nsIFile(this).lastModifiedTime; + }, + isBlockDevice: function() { + return nsIFile(this).isSpecial(); + }, + isCharacterDevice: function() { + return nsIFile(this).isSpecial(); + }, + isFIFO: function() { + return nsIFile(this).isSpecial(); + }, + isSocket: function() { + return nsIFile(this).isSpecial(); + }, + // non standard + get exists() { + return nsIFile(this).exists(); + }, + get hidden() { + return nsIFile(this).isHidden(); + }, + get writable() { + return nsIFile(this).isWritable(); + }, + get readable() { + return nsIFile(this).isReadable(); + } +}); +exports.Stats = Stats; + +const LStats = Class({ + extends: Stats, + get size() { + return this.isSymbolicLink() ? nsIFile(this).fileSizeOfLink : + nsIFile(this).fileSize; + }, + get mtime() { + return this.isSymbolicLink() ? nsIFile(this).lastModifiedTimeOfLink : + nsIFile(this).lastModifiedTime; + }, + // non standard + get permissions() { + return this.isSymbolicLink() ? nsIFile(this).permissionsOfLink : + nsIFile(this).permissions; + } +}); + +const FStat = Class({ + extends: Stats, + initialize: function initialize(fd) { + nsIFile(this, nsIFile(fd)); + } +}); + +function noop() {} +function Async(wrapped) { + return function (path, callback) { + let args = Array.slice(arguments); + callback = args.pop(); + // If node is not given a callback argument + // it just does not calls it. + if (typeof(callback) !== "function") { + args.push(callback); + callback = noop; + } + setTimeout(function() { + try { + var result = wrapped.apply(this, args); + if (result === undefined) callback(null); + else callback(null, result); + } catch (error) { + callback(error); + } + }, 0); + } +} + + +/** + * Synchronous rename(2) + */ +function renameSync(oldPath, newPath) { + let source = new nsILocalFile(oldPath); + let target = new nsILocalFile(newPath); + if (!source.exists()) throw FSError("rename", "ENOENT", 34, oldPath); + return source.moveTo(target.parent, target.leafName); +}; +exports.renameSync = renameSync; + +/** + * Asynchronous rename(2). No arguments other than a possible exception are + * given to the completion callback. + */ +var rename = Async(renameSync); +exports.rename = rename; + +/** + * Test whether or not the given path exists by checking with the file system. + */ +function existsSync(path) { + return new nsILocalFile(path).exists(); +} +exports.existsSync = existsSync; + +var exists = Async(existsSync); +exports.exists = exists; + +/** + * Synchronous ftruncate(2). + */ +function truncateSync(path, length) { + let fd = openSync(path, "w"); + ftruncateSync(fd, length); + closeSync(fd); +} +exports.truncateSync = truncateSync; + +/** + * Asynchronous ftruncate(2). No arguments other than a possible exception are + * given to the completion callback. + */ +function truncate(path, length, callback) { + open(path, "w", function(error, fd) { + if (error) return callback(error); + ftruncate(fd, length, function(error) { + if (error) { + closeSync(fd); + callback(error); + } + else { + close(fd, callback); + } + }); + }); +} +exports.truncate = truncate; + +function ftruncate(fd, length, callback) { + write(fd, new Buffer(length), 0, length, 0, function(error) { + callback(error); + }); +} +exports.ftruncate = ftruncate; + +function ftruncateSync(fd, length = 0) { + writeSync(fd, new Buffer(length), 0, length, 0); +} +exports.ftruncateSync = ftruncateSync; + +function chownSync(path, uid, gid) { + throw Error("Not implemented yet!!"); +} +exports.chownSync = chownSync; + +var chown = Async(chownSync); +exports.chown = chown; + +function lchownSync(path, uid, gid) { + throw Error("Not implemented yet!!"); +} +exports.lchownSync = chownSync; + +var lchown = Async(lchown); +exports.lchown = lchown; + +/** + * Synchronous chmod(2). + */ +function chmodSync (path, mode) { + let file; + try { + file = new nsILocalFile(path); + } catch(e) { + throw FSError("chmod", "ENOENT", 34, path); + } + + file.permissions = Mode(mode); +} +exports.chmodSync = chmodSync; +/** + * Asynchronous chmod(2). No arguments other than a possible exception are + * given to the completion callback. + */ +var chmod = Async(chmodSync); +exports.chmod = chmod; + +/** + * Synchronous chmod(2). + */ +function fchmodSync(fd, mode) { + throw Error("Not implemented yet!!"); +}; +exports.fchmodSync = fchmodSync; +/** + * Asynchronous chmod(2). No arguments other than a possible exception are + * given to the completion callback. + */ +var fchmod = Async(fchmodSync); +exports.fchmod = fchmod; + + +/** + * Synchronous stat(2). Returns an instance of `fs.Stats` + */ +function statSync(path) { + return new Stats(path); +}; +exports.statSync = statSync; + +/** + * Asynchronous stat(2). The callback gets two arguments (err, stats) where + * stats is a `fs.Stats` object. It looks like this: + */ +var stat = Async(statSync); +exports.stat = stat; + +/** + * Synchronous lstat(2). Returns an instance of `fs.Stats`. + */ +function lstatSync(path) { + return new LStats(path); +}; +exports.lstatSync = lstatSync; + +/** + * Asynchronous lstat(2). The callback gets two arguments (err, stats) where + * stats is a fs.Stats object. lstat() is identical to stat(), except that if + * path is a symbolic link, then the link itself is stat-ed, not the file that + * it refers to. + */ +var lstat = Async(lstatSync); +exports.lstat = lstat; + +/** + * Synchronous fstat(2). Returns an instance of `fs.Stats`. + */ +function fstatSync(fd) { + return new FStat(fd); +}; +exports.fstatSync = fstatSync; + +/** + * Asynchronous fstat(2). The callback gets two arguments (err, stats) where + * stats is a fs.Stats object. + */ +var fstat = Async(fstatSync); +exports.fstat = fstat; + +/** + * Synchronous link(2). + */ +function linkSync(source, target) { + throw Error("Not implemented yet!!"); +}; +exports.linkSync = linkSync; + +/** + * Asynchronous link(2). No arguments other than a possible exception are given + * to the completion callback. + */ +var link = Async(linkSync); +exports.link = link; + +/** + * Synchronous symlink(2). + */ +function symlinkSync(source, target) { + throw Error("Not implemented yet!!"); +}; +exports.symlinkSync = symlinkSync; + +/** + * Asynchronous symlink(2). No arguments other than a possible exception are + * given to the completion callback. + */ +var symlink = Async(symlinkSync); +exports.symlink = symlink; + +/** + * Synchronous readlink(2). Returns the resolved path. + */ +function readlinkSync(path) { + return new nsILocalFile(path).target; +}; +exports.readlinkSync = readlinkSync; + +/** + * Asynchronous readlink(2). The callback gets two arguments + * `(error, resolvedPath)`. + */ +var readlink = Async(readlinkSync); +exports.readlink = readlink; + +/** + * Synchronous realpath(2). Returns the resolved path. + */ +function realpathSync(path) { + return new nsILocalFile(path).path; +}; +exports.realpathSync = realpathSync; + +/** + * Asynchronous realpath(2). The callback gets two arguments + * `(err, resolvedPath)`. + */ +var realpath = Async(realpathSync); +exports.realpath = realpath; + +/** + * Synchronous unlink(2). + */ +var unlinkSync = remove; +exports.unlinkSync = unlinkSync; + +/** + * Asynchronous unlink(2). No arguments other than a possible exception are + * given to the completion callback. + */ +var unlink = Async(remove); +exports.unlink = unlink; + +/** + * Synchronous rmdir(2). + */ +var rmdirSync = remove; +exports.rmdirSync = rmdirSync; + +/** + * Asynchronous rmdir(2). No arguments other than a possible exception are + * given to the completion callback. + */ +var rmdir = Async(rmdirSync); +exports.rmdir = rmdir; + +/** + * Synchronous mkdir(2). + */ +function mkdirSync(path, mode) { + try { + return nsILocalFile(path).create(DIRECTORY_TYPE, Mode(mode)); + } catch (error) { + // Adjust exception thorw to match ones thrown by node. + if (error.name === "NS_ERROR_FILE_ALREADY_EXISTS") { + let { fileName, lineNumber } = error; + error = FSError("mkdir", "EEXIST", 47, path, fileName, lineNumber); + } + throw error; + } +}; +exports.mkdirSync = mkdirSync; + +/** + * Asynchronous mkdir(2). No arguments other than a possible exception are + * given to the completion callback. + */ +var mkdir = Async(mkdirSync); +exports.mkdir = mkdir; + +/** + * Synchronous readdir(3). Returns an array of filenames excluding `"."` and + * `".."`. + */ +function readdirSync(path) { + try { + return toArray(new nsILocalFile(path).directoryEntries).map(getFileName); + } + catch (error) { + // Adjust exception thorw to match ones thrown by node. + if (error.name === "NS_ERROR_FILE_TARGET_DOES_NOT_EXIST" || + error.name === "NS_ERROR_FILE_NOT_FOUND") + { + let { fileName, lineNumber } = error; + error = FSError("readdir", "ENOENT", 34, path, fileName, lineNumber); + } + throw error; + } +}; +exports.readdirSync = readdirSync; + +/** + * Asynchronous readdir(3). Reads the contents of a directory. The callback + * gets two arguments `(error, files)` where `files` is an array of the names + * of the files in the directory excluding `"."` and `".."`. + */ +var readdir = Async(readdirSync); +exports.readdir = readdir; + +/** + * Synchronous close(2). + */ + function closeSync(fd) { + let input = nsIFileInputStream(fd); + let output = nsIFileOutputStream(fd); + + // Closing input stream and removing reference. + if (input) input.close(); + // Closing output stream and removing reference. + if (output) output.close(); + + nsIFile(fd, null); + nsIFileInputStream(fd, null); + nsIFileOutputStream(fd, null); + nsIBinaryInputStream(fd, null); + nsIBinaryOutputStream(fd, null); +}; +exports.closeSync = closeSync; +/** + * Asynchronous close(2). No arguments other than a possible exception are + * given to the completion callback. + */ +var close = Async(closeSync); +exports.close = close; + +/** + * Synchronous open(2). + */ +function openSync(aPath, aFlag, aMode) { + let [ fd, flags, mode, file ] = + [ { path: aPath }, Flags(aFlag), Mode(aMode), nsILocalFile(aPath) ]; + + nsIFile(fd, file); + + // If trying to open file for just read that does not exists + // need to throw exception as node does. + if (!file.exists() && !isWritable(flags)) + throw FSError("open", "ENOENT", 34, aPath); + + // If we want to open file in read mode we initialize input stream. + if (isReadable(flags)) { + let input = FileInputStream(file, flags, mode, DEFER_OPEN); + nsIFileInputStream(fd, input); + } + + // If we want to open file in write mode we initialize output stream for it. + if (isWritable(flags)) { + let output = FileOutputStream(file, flags, mode, DEFER_OPEN); + nsIFileOutputStream(fd, output); + } + + return fd; +} +exports.openSync = openSync; +/** + * Asynchronous file open. See open(2). Flags can be + * `"r", "r+", "w", "w+", "a"`, or `"a+"`. mode defaults to `0666`. + * The callback gets two arguments `(error, fd). + */ +var open = Async(openSync); +exports.open = open; + +/** + * Synchronous version of buffer-based fs.write(). Returns the number of bytes + * written. + */ +function writeSync(fd, buffer, offset, length, position) { + if (length + offset > buffer.length) { + throw Error("Length is extends beyond buffer"); + } + else if (length + offset !== buffer.length) { + buffer = buffer.slice(offset, offset + length); + } + + let output = BinaryOutputStream(nsIFileOutputStream(fd)); + nsIBinaryOutputStream(fd, output); + // We write content as a byte array as this will avoid any transcoding + // if content was a buffer. + output.writeByteArray(buffer.valueOf(), buffer.length); + output.flush(); +}; +exports.writeSync = writeSync; + +/** + * Write buffer to the file specified by fd. + * + * `offset` and `length` determine the part of the buffer to be written. + * + * `position` refers to the offset from the beginning of the file where this + * data should be written. If `position` is `null`, the data will be written + * at the current position. See pwrite(2). + * + * The callback will be given three arguments `(error, written, buffer)` where + * written specifies how many bytes were written into buffer. + * + * Note that it is unsafe to use `fs.write` multiple times on the same file + * without waiting for the callback. + */ +function write(fd, buffer, offset, length, position, callback) { + if (!Buffer.isBuffer(buffer)) { + // (fd, data, position, encoding, callback) + let encoding = null; + [ position, encoding, callback ] = Array.slice(arguments, 1); + buffer = new Buffer(String(buffer), encoding); + offset = 0; + } else if (length + offset > buffer.length) { + throw Error("Length is extends beyond buffer"); + } else if (length + offset !== buffer.length) { + buffer = buffer.slice(offset, offset + length); + } + + let writeStream = new WriteStream(fd, { position: position, + length: length }); + writeStream.on("error", callback); + writeStream.write(buffer, function onEnd() { + writeStream.destroy(); + if (callback) + callback(null, buffer.length, buffer); + }); +}; +exports.write = write; + +/** + * Synchronous version of string-based fs.read. Returns the number of + * bytes read. + */ +function readSync(fd, buffer, offset, length, position) { + let input = nsIFileInputStream(fd); + // Setting a stream position, unless it"s `-1` which means current position. + if (position >= 0) + input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position); + // We use `nsIStreamTransportService` service to transform blocking + // file input stream into a fully asynchronous stream that can be written + // without blocking the main thread. + let binaryInputStream = BinaryInputStream(input); + let count = length === ALL ? binaryInputStream.available() : length; + if (offset === 0) binaryInputStream.readArrayBuffer(count, buffer.buffer); + else { + let chunk = new Buffer(count); + binaryInputStream.readArrayBuffer(count, chunk.buffer); + chunk.copy(buffer, offset); + } + + return buffer.slice(offset, offset + count); +}; +exports.readSync = readSync; + +/** + * Read data from the file specified by `fd`. + * + * `buffer` is the buffer that the data will be written to. + * `offset` is offset within the buffer where writing will start. + * + * `length` is an integer specifying the number of bytes to read. + * + * `position` is an integer specifying where to begin reading from in the file. + * If `position` is `null`, data will be read from the current file position. + * + * The callback is given the three arguments, `(error, bytesRead, buffer)`. + */ +function read(fd, buffer, offset, length, position, callback) { + let bytesRead = 0; + let readStream = new ReadStream(fd, { position: position, length: length }); + readStream.on("data", function onData(data) { + data.copy(buffer, offset + bytesRead); + bytesRead += data.length; + }); + readStream.on("end", function onEnd() { + callback(null, bytesRead, buffer); + readStream.destroy(); + }); +}; +exports.read = read; + +/** + * Asynchronously reads the entire contents of a file. + * The callback is passed two arguments `(error, data)`, where data is the + * contents of the file. + */ +function readFile(path, encoding, callback) { + if (isFunction(encoding)) { + callback = encoding + encoding = null + } + + let buffer = null; + try { + let readStream = new ReadStream(path); + readStream.on("data", function(data) { + if (!buffer) buffer = data; + else buffer = Buffer.concat([buffer, data], 2); + }); + readStream.on("error", function onError(error) { + callback(error); + }); + readStream.on("end", function onEnd() { + // Note: Need to destroy before invoking a callback + // so that file descriptor is released. + readStream.destroy(); + callback(null, buffer); + }); + } + catch (error) { + setTimeout(callback, 0, error); + } +}; +exports.readFile = readFile; + +/** + * Synchronous version of `fs.readFile`. Returns the contents of the path. + * If encoding is specified then this function returns a string. + * Otherwise it returns a buffer. + */ +function readFileSync(path, encoding) { + let fd = openSync(path, "r"); + let size = fstatSync(fd).size; + let buffer = new Buffer(size); + try { + readSync(fd, buffer, 0, ALL, 0); + } + finally { + closeSync(fd); + } + return buffer; +}; +exports.readFileSync = readFileSync; + +/** + * Asynchronously writes data to a file, replacing the file if it already + * exists. data can be a string or a buffer. + */ +function writeFile(path, content, encoding, callback) { + if (!isString(path)) + throw new TypeError('path must be a string'); + + try { + if (isFunction(encoding)) { + callback = encoding + encoding = null + } + if (isString(content)) + content = new Buffer(content, encoding); + + let writeStream = new WriteStream(path); + let error = null; + + writeStream.end(content, function() { + writeStream.destroy(); + callback(error); + }); + + writeStream.on("error", function onError(reason) { + error = reason; + writeStream.destroy(); + }); + } catch (error) { + callback(error); + } +}; +exports.writeFile = writeFile; + +/** + * The synchronous version of `fs.writeFile`. + */ +function writeFileSync(filename, data, encoding) { + // TODO: Implement this in bug 1148209 https://bugzilla.mozilla.org/show_bug.cgi?id=1148209 + throw Error("Not implemented"); +}; +exports.writeFileSync = writeFileSync; + + +function utimesSync(path, atime, mtime) { + throw Error("Not implemented"); +} +exports.utimesSync = utimesSync; + +var utimes = Async(utimesSync); +exports.utimes = utimes; + +function futimesSync(fd, atime, mtime, callback) { + throw Error("Not implemented"); +} +exports.futimesSync = futimesSync; + +var futimes = Async(futimesSync); +exports.futimes = futimes; + +function fsyncSync(fd, atime, mtime, callback) { + throw Error("Not implemented"); +} +exports.fsyncSync = fsyncSync; + +var fsync = Async(fsyncSync); +exports.fsync = fsync; + + +/** + * Watch for changes on filename. The callback listener will be called each + * time the file is accessed. + * + * The second argument is optional. The options if provided should be an object + * containing two members a boolean, persistent, and interval, a polling value + * in milliseconds. The default is { persistent: true, interval: 0 }. + */ +function watchFile(path, options, listener) { + throw Error("Not implemented"); +}; +exports.watchFile = watchFile; + + +function unwatchFile(path, listener) { + throw Error("Not implemented"); +} +exports.unwatchFile = unwatchFile; + +function watch(path, options, listener) { + throw Error("Not implemented"); +} +exports.watch = watch; diff --git a/addon-sdk/source/lib/sdk/io/stream.js b/addon-sdk/source/lib/sdk/io/stream.js new file mode 100644 index 000000000..0698b8e32 --- /dev/null +++ b/addon-sdk/source/lib/sdk/io/stream.js @@ -0,0 +1,440 @@ +/* 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 { CC, Cc, Ci, Cu, Cr, components } = require("chrome"); +const { EventTarget } = require("../event/target"); +const { emit } = require("../event/core"); +const { Buffer } = require("./buffer"); +const { Class } = require("../core/heritage"); +const { setTimeout } = require("../timers"); + + +const MultiplexInputStream = CC("@mozilla.org/io/multiplex-input-stream;1", + "nsIMultiplexInputStream"); +const AsyncStreamCopier = CC("@mozilla.org/network/async-stream-copier;1", + "nsIAsyncStreamCopier", "init"); +const StringInputStream = CC("@mozilla.org/io/string-input-stream;1", + "nsIStringInputStream"); +const ArrayBufferInputStream = CC("@mozilla.org/io/arraybuffer-input-stream;1", + "nsIArrayBufferInputStream"); + +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", "setInputStream"); +const InputStreamPump = CC("@mozilla.org/network/input-stream-pump;1", + "nsIInputStreamPump", "init"); + +const threadManager = Cc["@mozilla.org/thread-manager;1"]. + getService(Ci.nsIThreadManager); + +const eventTarget = Cc["@mozilla.org/network/stream-transport-service;1"]. + getService(Ci.nsIEventTarget); + +var isFunction = value => typeof(value) === "function" + +function accessor() { + let map = new WeakMap(); + return function(target, value) { + if (value) + map.set(target, value); + return map.get(target); + } +} + +const Stream = Class({ + extends: EventTarget, + initialize: function() { + this.readable = false; + this.writable = false; + this.encoding = null; + }, + setEncoding: function setEncoding(encoding) { + this.encoding = String(encoding).toUpperCase(); + }, + pipe: function pipe(target, options) { + let source = this; + function onData(chunk) { + if (target.writable) { + if (false === target.write(chunk)) + source.pause(); + } + } + function onDrain() { + if (source.readable) + source.resume(); + } + function onEnd() { + target.end(); + } + function onPause() { + source.pause(); + } + function onResume() { + if (source.readable) + source.resume(); + } + + function cleanup() { + source.removeListener("data", onData); + target.removeListener("drain", onDrain); + source.removeListener("end", onEnd); + + target.removeListener("pause", onPause); + target.removeListener("resume", onResume); + + source.removeListener("end", cleanup); + source.removeListener("close", cleanup); + + target.removeListener("end", cleanup); + target.removeListener("close", cleanup); + } + + if (!options || options.end !== false) + target.on("end", onEnd); + + source.on("data", onData); + target.on("drain", onDrain); + target.on("resume", onResume); + target.on("pause", onPause); + + source.on("end", cleanup); + source.on("close", cleanup); + + target.on("end", cleanup); + target.on("close", cleanup); + + emit(target, "pipe", source); + }, + pause: function pause() { + emit(this, "pause"); + }, + resume: function resume() { + emit(this, "resume"); + }, + destroySoon: function destroySoon() { + this.destroy(); + } +}); +exports.Stream = Stream; + + +var nsIStreamListener = accessor(); +var nsIInputStreamPump = accessor(); +var nsIAsyncInputStream = accessor(); +var nsIBinaryInputStream = accessor(); + +const StreamListener = Class({ + initialize: function(stream) { + this.stream = stream; + }, + + // Next three methods are part of `nsIStreamListener` interface and are + // invoked by `nsIInputStreamPump.asyncRead`. + onDataAvailable: function(request, context, input, offset, count) { + let stream = this.stream; + let buffer = new ArrayBuffer(count); + nsIBinaryInputStream(stream).readArrayBuffer(count, buffer); + emit(stream, "data", new Buffer(buffer)); + }, + + // Next two methods implement `nsIRequestObserver` interface and are invoked + // by `nsIInputStreamPump.asyncRead`. + onStartRequest: function() {}, + // Called to signify the end of an asynchronous request. We only care to + // discover errors. + onStopRequest: function(request, context, status) { + let stream = this.stream; + stream.readable = false; + if (!components.isSuccessCode(status)) + emit(stream, "error", status); + else + emit(stream, "end"); + } +}); + + +const InputStream = Class({ + extends: Stream, + readable: false, + paused: false, + initialize: function initialize(options) { + let { asyncInputStream } = options; + + this.readable = true; + + let binaryInputStream = new BinaryInputStream(asyncInputStream); + let inputStreamPump = new InputStreamPump(asyncInputStream, + -1, -1, 0, 0, false); + let streamListener = new StreamListener(this); + + nsIAsyncInputStream(this, asyncInputStream); + nsIInputStreamPump(this, inputStreamPump); + nsIBinaryInputStream(this, binaryInputStream); + nsIStreamListener(this, streamListener); + + this.asyncInputStream = asyncInputStream; + this.inputStreamPump = inputStreamPump; + this.binaryInputStream = binaryInputStream; + }, + get status() { + return nsIInputStreamPump(this).status; + }, + read: function() { + nsIInputStreamPump(this).asyncRead(nsIStreamListener(this), null); + }, + pause: function pause() { + this.paused = true; + nsIInputStreamPump(this).suspend(); + emit(this, "paused"); + }, + resume: function resume() { + this.paused = false; + if (nsIInputStreamPump(this).isPending()) { + nsIInputStreamPump(this).resume(); + emit(this, "resume"); + } + }, + close: function close() { + this.readable = false; + nsIInputStreamPump(this).cancel(Cr.NS_OK); + nsIBinaryInputStream(this).close(); + nsIAsyncInputStream(this).close(); + }, + destroy: function destroy() { + this.close(); + + nsIInputStreamPump(this); + nsIAsyncInputStream(this); + nsIBinaryInputStream(this); + nsIStreamListener(this); + } +}); +exports.InputStream = InputStream; + + + +var nsIRequestObserver = accessor(); +var nsIAsyncOutputStream = accessor(); +var nsIAsyncStreamCopier = accessor(); +var nsIMultiplexInputStream = accessor(); + +const RequestObserver = Class({ + initialize: function(stream) { + this.stream = stream; + }, + // Method is part of `nsIRequestObserver` interface that is + // invoked by `nsIAsyncStreamCopier.asyncCopy`. + onStartRequest: function() {}, + // Method is part of `nsIRequestObserver` interface that is + // invoked by `nsIAsyncStreamCopier.asyncCopy`. + onStopRequest: function(request, context, status) { + let stream = this.stream; + stream.drained = true; + + // Remove copied chunk. + let multiplexInputStream = nsIMultiplexInputStream(stream); + multiplexInputStream.removeStream(0); + + // If there was an error report. + if (!components.isSuccessCode(status)) + emit(stream, "error", status); + + // If there more chunks in queue then flush them. + else if (multiplexInputStream.count) + stream.flush(); + + // If stream is still writable notify that queue has drained. + else if (stream.writable) + emit(stream, "drain"); + + // If stream is no longer writable close it. + else { + nsIAsyncStreamCopier(stream).cancel(Cr.NS_OK); + nsIMultiplexInputStream(stream).close(); + nsIAsyncOutputStream(stream).close(); + nsIAsyncOutputStream(stream).flush(); + } + } +}); + +const OutputStreamCallback = Class({ + initialize: function(stream) { + this.stream = stream; + }, + // Method is part of `nsIOutputStreamCallback` interface that + // is invoked by `nsIAsyncOutputStream.asyncWait`. It is registered + // with `WAIT_CLOSURE_ONLY` flag that overrides the default behavior, + // causing the `onOutputStreamReady` notification to be suppressed until + // the stream becomes closed. + onOutputStreamReady: function(nsIAsyncOutputStream) { + emit(this.stream, "finish"); + } +}); + +const OutputStream = Class({ + extends: Stream, + writable: false, + drained: true, + get bufferSize() { + let multiplexInputStream = nsIMultiplexInputStream(this); + return multiplexInputStream && multiplexInputStream.available(); + }, + initialize: function initialize(options) { + let { asyncOutputStream, output } = options; + this.writable = true; + + // Ensure that `nsIAsyncOutputStream` was provided. + asyncOutputStream.QueryInterface(Ci.nsIAsyncOutputStream); + + // Create a `nsIMultiplexInputStream` and `nsIAsyncStreamCopier`. Former + // is used to queue written data chunks that `asyncStreamCopier` will + // asynchronously drain into `asyncOutputStream`. + let multiplexInputStream = MultiplexInputStream(); + let asyncStreamCopier = AsyncStreamCopier(multiplexInputStream, + output || asyncOutputStream, + eventTarget, + // nsIMultiplexInputStream + // implemnts .readSegments() + true, + // nsIOutputStream may or + // may not implemnet + // .writeSegments(). + false, + // Use default buffer size. + null, + // Should not close an input. + false, + // Should not close an output. + false); + + // Create `requestObserver` implementing `nsIRequestObserver` interface + // in the constructor that's gonna be reused across several flushes. + let requestObserver = RequestObserver(this); + + + // Create observer that implements `nsIOutputStreamCallback` and register + // using `WAIT_CLOSURE_ONLY` flag. That way it will be notfied once + // `nsIAsyncOutputStream` is closed. + asyncOutputStream.asyncWait(OutputStreamCallback(this), + asyncOutputStream.WAIT_CLOSURE_ONLY, + 0, + threadManager.currentThread); + + nsIRequestObserver(this, requestObserver); + nsIAsyncOutputStream(this, asyncOutputStream); + nsIMultiplexInputStream(this, multiplexInputStream); + nsIAsyncStreamCopier(this, asyncStreamCopier); + + this.asyncOutputStream = asyncOutputStream; + this.multiplexInputStream = multiplexInputStream; + this.asyncStreamCopier = asyncStreamCopier; + }, + write: function write(content, encoding, callback) { + if (isFunction(encoding)) { + callback = encoding; + encoding = callback; + } + + // If stream is not writable we throw an error. + if (!this.writable) throw Error("stream is not writable"); + + let chunk = null; + + // If content is not a buffer then we create one out of it. + if (Buffer.isBuffer(content)) { + chunk = new ArrayBufferInputStream(); + chunk.setData(content.buffer, 0, content.length); + } + else { + chunk = new StringInputStream(); + chunk.setData(content, content.length); + } + + if (callback) + this.once("drain", callback); + + // Queue up chunk to be copied to output sync. + nsIMultiplexInputStream(this).appendStream(chunk); + this.flush(); + + return this.drained; + }, + flush: function() { + if (this.drained) { + this.drained = false; + nsIAsyncStreamCopier(this).asyncCopy(nsIRequestObserver(this), null); + } + }, + end: function end(content, encoding, callback) { + if (isFunction(content)) { + callback = content + content = callback + } + if (isFunction(encoding)) { + callback = encoding + encoding = callback + } + + // Setting a listener to "finish" event if passed. + if (isFunction(callback)) + this.once("finish", callback); + + + if (content) + this.write(content, encoding); + this.writable = false; + + // Close `asyncOutputStream` only if output has drained. If it's + // not drained than `asyncStreamCopier` is busy writing, so let + // it finish. Note that since `this.writable` is false copier will + // close `asyncOutputStream` once output drains. + if (this.drained) + nsIAsyncOutputStream(this).close(); + }, + destroy: function destroy() { + nsIAsyncOutputStream(this).close(); + nsIAsyncOutputStream(this); + nsIMultiplexInputStream(this); + nsIAsyncStreamCopier(this); + nsIRequestObserver(this); + } +}); +exports.OutputStream = OutputStream; + +const DuplexStream = Class({ + extends: Stream, + implements: [InputStream, OutputStream], + allowHalfOpen: true, + initialize: function initialize(options) { + options = options || {}; + let { readable, writable, allowHalfOpen } = options; + + InputStream.prototype.initialize.call(this, options); + OutputStream.prototype.initialize.call(this, options); + + if (readable === false) + this.readable = false; + + if (writable === false) + this.writable = false; + + if (allowHalfOpen === false) + this.allowHalfOpen = false; + + // If in a half open state and it's disabled enforce end. + this.once("end", () => { + if (!this.allowHalfOpen && (!this.readable || !this.writable)) + this.end(); + }); + }, + destroy: function destroy(error) { + InputStream.prototype.destroy.call(this); + OutputStream.prototype.destroy.call(this); + } +}); +exports.DuplexStream = DuplexStream; diff --git a/addon-sdk/source/lib/sdk/io/text-streams.js b/addon-sdk/source/lib/sdk/io/text-streams.js new file mode 100644 index 000000000..ed4ec4972 --- /dev/null +++ b/addon-sdk/source/lib/sdk/io/text-streams.js @@ -0,0 +1,235 @@ +/* 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 { Cc, Ci, Cu, components } = require("chrome"); +const { ensure } = require("../system/unload"); +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); + +// NetUtil.asyncCopy() uses this buffer length, and since we call it, for best +// performance we use it, too. +const BUFFER_BYTE_LEN = 0x8000; +const PR_UINT32_MAX = 0xffffffff; +const DEFAULT_CHARSET = "UTF-8"; + + +/** + * An input stream that reads text from a backing stream using a given text + * encoding. + * + * @param inputStream + * The stream is backed by this nsIInputStream. It must already be + * opened. + * @param charset + * Text in inputStream is expected to be in this character encoding. If + * not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl for + * documentation on how to determine other valid values for this. + */ +function TextReader(inputStream, charset) { + charset = checkCharset(charset); + + let stream = Cc["@mozilla.org/intl/converter-input-stream;1"]. + createInstance(Ci.nsIConverterInputStream); + stream.init(inputStream, charset, BUFFER_BYTE_LEN, + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + + let manager = new StreamManager(this, stream); + + /** + * Reads a string from the stream. If the stream is closed, an exception is + * thrown. + * + * @param numChars + * The number of characters to read. If not given, the remainder of + * the stream is read. + * @return The string read. If the stream is already at EOS, returns the + * empty string. + */ + this.read = function TextReader_read(numChars) { + manager.ensureOpened(); + + let readAll = false; + if (typeof(numChars) === "number") + numChars = Math.max(numChars, 0); + else + readAll = true; + + let str = ""; + let totalRead = 0; + let chunkRead = 1; + + // Read in numChars or until EOS, whichever comes first. Note that the + // units here are characters, not bytes. + while (true) { + let chunk = {}; + let toRead = readAll ? + PR_UINT32_MAX : + Math.min(numChars - totalRead, PR_UINT32_MAX); + if (toRead <= 0 || chunkRead <= 0) + break; + + // The converter stream reads in at most BUFFER_BYTE_LEN bytes in a call + // to readString, enough to fill its byte buffer. chunkRead will be the + // number of characters encoded by the bytes in that buffer. + chunkRead = stream.readString(toRead, chunk); + str += chunk.value; + totalRead += chunkRead; + } + + return str; + }; +} +exports.TextReader = TextReader; + +/** + * A buffered output stream that writes text to a backing stream using a given + * text encoding. + * + * @param outputStream + * The stream is backed by this nsIOutputStream. It must already be + * opened. + * @param charset + * Text will be written to outputStream using this character encoding. + * If not given, "UTF-8" is assumed. See nsICharsetConverterManager.idl + * for documentation on how to determine other valid values for this. + */ +function TextWriter(outputStream, charset) { + charset = checkCharset(charset); + + let stream = outputStream; + + // Buffer outputStream if it's not already. + let ioUtils = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil); + if (!ioUtils.outputStreamIsBuffered(outputStream)) { + stream = Cc["@mozilla.org/network/buffered-output-stream;1"]. + createInstance(Ci.nsIBufferedOutputStream); + stream.init(outputStream, BUFFER_BYTE_LEN); + } + + // I'd like to use nsIConverterOutputStream. But NetUtil.asyncCopy(), which + // we use below in writeAsync(), naturally expects its sink to be an instance + // of nsIOutputStream, which nsIConverterOutputStream's only implementation is + // not. So we use uconv and manually convert all strings before writing to + // outputStream. + let uconv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + uconv.charset = charset; + + let manager = new StreamManager(this, stream); + + /** + * Flushes the backing stream's buffer. + */ + this.flush = function TextWriter_flush() { + manager.ensureOpened(); + stream.flush(); + }; + + /** + * Writes a string to the stream. If the stream is closed, an exception is + * thrown. + * + * @param str + * The string to write. + */ + this.write = function TextWriter_write(str) { + manager.ensureOpened(); + let istream = uconv.convertToInputStream(str); + let len = istream.available(); + while (len > 0) { + stream.writeFrom(istream, len); + len = istream.available(); + } + istream.close(); + }; + + /** + * Writes a string on a background thread. After the write completes, the + * backing stream's buffer is flushed, and both the stream and the backing + * stream are closed, also on the background thread. If the stream is already + * closed, an exception is thrown immediately. + * + * @param str + * The string to write. + * @param callback + * An optional function. If given, it's called as callback(error) when + * the write completes. error is an Error object or undefined if there + * was no error. Inside callback, |this| is the stream object. + */ + this.writeAsync = function TextWriter_writeAsync(str, callback) { + manager.ensureOpened(); + let istream = uconv.convertToInputStream(str); + NetUtil.asyncCopy(istream, stream, (result) => { + let err = components.isSuccessCode(result) ? undefined : + new Error("An error occured while writing to the stream: " + result); + if (err) + console.error(err); + + // asyncCopy() closes its output (and input) stream. + manager.opened = false; + + if (typeof(callback) === "function") { + try { + callback.call(this, err); + } + catch (exc) { + console.exception(exc); + } + } + }); + }; +} +exports.TextWriter = TextWriter; + +// This manages the lifetime of stream, a TextReader or TextWriter. It defines +// closed and close() on stream and registers an unload listener that closes +// rawStream if it's still opened. It also provides ensureOpened(), which +// throws an exception if the stream is closed. +function StreamManager(stream, rawStream) { + this.rawStream = rawStream; + this.opened = true; + + /** + * True iff the stream is closed. + */ + stream.__defineGetter__("closed", () => !this.opened); + + /** + * Closes both the stream and its backing stream. If the stream is already + * closed, an exception is thrown. For TextWriters, this first flushes the + * backing stream's buffer. + */ + stream.close = () => { + this.ensureOpened(); + this.unload(); + }; + + ensure(this); +} + +StreamManager.prototype = { + ensureOpened: function StreamManager_ensureOpened() { + if (!this.opened) + throw new Error("The stream is closed and cannot be used."); + }, + unload: function StreamManager_unload() { + // TextWriter.writeAsync() causes rawStream to close and therefore sets + // opened to false, so check that we're still opened. + if (this.opened) { + // Calling close() on both an nsIUnicharInputStream and + // nsIBufferedOutputStream closes their backing streams. It also forces + // nsIOutputStreams to flush first. + this.rawStream.close(); + this.opened = false; + } + } +}; + +function checkCharset(charset) { + return typeof(charset) === "string" ? charset : DEFAULT_CHARSET; +} diff --git a/addon-sdk/source/lib/sdk/keyboard/hotkeys.js b/addon-sdk/source/lib/sdk/keyboard/hotkeys.js new file mode 100644 index 000000000..a179502b8 --- /dev/null +++ b/addon-sdk/source/lib/sdk/keyboard/hotkeys.js @@ -0,0 +1,110 @@ +/* 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 { observer: keyboardObserver } = require("./observer"); +const { getKeyForCode, normalize, isFunctionKey, + MODIFIERS } = require("./utils"); + +/** + * Register a global `hotkey` that executes `listener` when the key combination + * in `hotkey` is pressed. If more then one `listener` is registered on the same + * key combination only last one will be executed. + * + * @param {string} hotkey + * Key combination in the format of 'modifier key'. + * + * Examples: + * + * "accel s" + * "meta shift i" + * "control alt d" + * + * Modifier keynames: + * + * - **shift**: The Shift key. + * - **alt**: The Alt key. On the Macintosh, this is the Option key. On + * Macintosh this can only be used in conjunction with another modifier, + * since `Alt+Letter` combinations are reserved for entering special + * characters in text. + * - **meta**: The Meta key. On the Macintosh, this is the Command key. + * - **control**: The Control key. + * - **accel**: The key used for keyboard shortcuts on the user's platform, + * which is Control on Windows and Linux, and Command on Mac. Usually, this + * would be the value you would use. + * + * @param {function} listener + * Function to execute when the `hotkey` is executed. + */ +exports.register = function register(hotkey, listener) { + hotkey = normalize(hotkey); + hotkeys[hotkey] = listener; +}; + +/** + * Unregister a global `hotkey`. If passed `listener` is not the one registered + * for the given `hotkey`, the call to this function will be ignored. + * + * @param {string} hotkey + * Key combination in the format of 'modifier key'. + * @param {function} listener + * Function that will be invoked when the `hotkey` is pressed. + */ +exports.unregister = function unregister(hotkey, listener) { + hotkey = normalize(hotkey); + if (hotkeys[hotkey] === listener) + delete hotkeys[hotkey]; +}; + +/** + * Map of hotkeys and associated functions. + */ +const hotkeys = exports.hotkeys = {}; + +keyboardObserver.on("keydown", function onKeypress(event, window) { + let key, modifiers = []; + let isChar = "isChar" in event && event.isChar; + let which = "which" in event ? event.which : null; + let keyCode = "keyCode" in event ? event.keyCode : null; + + if ("shiftKey" in event && event.shiftKey) + modifiers.push("shift"); + if ("altKey" in event && event.altKey) + modifiers.push("alt"); + if ("ctrlKey" in event && event.ctrlKey) + modifiers.push("control"); + if ("metaKey" in event && event.metaKey) + modifiers.push("meta"); + + // If it's not a printable character then we fall back to a human readable + // equivalent of one of the following constants. + // http://dxr.mozilla.org/mozilla-central/source/dom/interfaces/events/nsIDOMKeyEvent.idl + key = getKeyForCode(keyCode); + + // If only non-function (f1 - f24) key or only modifiers are pressed we don't + // have a valid combination so we return immediately (Also, sometimes + // `keyCode` may be one for the modifier which means we do not have a + // modifier). + if (!key || (!isFunctionKey(key) && !modifiers.length) || key in MODIFIERS) + return; + + let combination = normalize({ key: key, modifiers: modifiers }); + let hotkey = hotkeys[combination]; + + if (hotkey) { + try { + hotkey(); + } catch (exception) { + console.exception(exception); + } finally { + // Work around bug 582052 by preventing the (nonexistent) default action. + event.preventDefault(); + } + } +}); diff --git a/addon-sdk/source/lib/sdk/keyboard/observer.js b/addon-sdk/source/lib/sdk/keyboard/observer.js new file mode 100644 index 000000000..b8e32b95c --- /dev/null +++ b/addon-sdk/source/lib/sdk/keyboard/observer.js @@ -0,0 +1,58 @@ +/* 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 { 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'); +const { observer: windowObserver } = require("../windows/observer"); + +// Event emitter objects used to register listeners and emit events on them +// when they occur. +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. + */ + supportedEventsTypes: [ "keydown", "keyup", "keypress" ], + /** + * Function handles all the supported events on all the windows that are + * observed. Method is used to proxy events to the listeners registered on + * this event emitter. + * @param {Event} event + * Keyboard event being emitted. + */ + handleEvent(event) { + emit(this, event.type, event, event.target.ownerDocument ? event.target.ownerDocument.defaultView + : undefined); + } +}); + +exports.observer = new Observer(); diff --git a/addon-sdk/source/lib/sdk/keyboard/utils.js b/addon-sdk/source/lib/sdk/keyboard/utils.js new file mode 100644 index 000000000..1b7df4ce3 --- /dev/null +++ b/addon-sdk/source/lib/sdk/keyboard/utils.js @@ -0,0 +1,189 @@ +/* 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 runtime = require("../system/runtime"); +const { isString } = require("../lang/type"); +const array = require("../util/array"); + + +const SWP = "{{SEPARATOR}}"; +const SEPARATOR = "-" +const INVALID_COMBINATION = "Hotkey key combination must contain one or more " + + "modifiers and only one key"; + +// Map of modifier key mappings. +const MODIFIERS = exports.MODIFIERS = { + 'accel': runtime.OS === "Darwin" ? 'meta' : 'control', + 'meta': 'meta', + 'control': 'control', + 'ctrl': 'control', + 'option': 'alt', + 'command': 'meta', + 'alt': 'alt', + 'shift': 'shift' +}; + +// Hash of key:code pairs for all the chars supported by `nsIDOMKeyEvent`. +// This is just a copy of the `nsIDOMKeyEvent` hash with normalized names. +// @See: http://dxr.mozilla.org/mozilla-central/source/dom/interfaces/events/nsIDOMKeyEvent.idl +const CODES = exports.CODES = new function Codes() { + let nsIDOMKeyEvent = Ci.nsIDOMKeyEvent; + // Names that will be substituted with a shorter analogs. + let aliases = { + 'subtract': '-', + 'add': '+', + 'equals': '=', + 'slash': '/', + 'backslash': '\\', + 'openbracket': '[', + 'closebracket': ']', + 'quote': '\'', + 'backquote': '`', + 'period': '.', + 'semicolon': ';', + 'comma': ',' + }; + + // Normalizing keys and copying values to `this` object. + Object.keys(nsIDOMKeyEvent).filter(function(key) { + // Filter out only key codes. + return key.indexOf('DOM_VK') === 0; + }).map(function(key) { + // Map to key:values + return [ key, nsIDOMKeyEvent[key] ]; + }).map(function([key, value]) { + return [ key.replace('DOM_VK_', '').replace('_', '').toLowerCase(), value ]; + }).forEach(function ([ key, value ]) { + this[aliases[key] || key] = value; + }, this); +}; + +// Inverted `CODES` hash of `code:key`. +const KEYS = exports.KEYS = new function Keys() { + Object.keys(CODES).forEach(function(key) { + this[CODES[key]] = key; + }, this) +} + +exports.getKeyForCode = function getKeyForCode(code) { + return (code in KEYS) && KEYS[code]; +}; +exports.getCodeForKey = function getCodeForKey(key) { + return (key in CODES) && CODES[key]; +}; + +/** + * Utility function that takes string or JSON that defines a `hotkey` and + * returns normalized string version of it. + * @param {JSON|String} hotkey + * @param {String} [separator=" "] + * Optional string that represents separator used to concatenate keys in the + * given `hotkey`. + * @returns {String} + * @examples + * + * require("keyboard/hotkeys").normalize("b Shift accel"); + * // 'control shift b' -> on windows & linux + * // 'meta shift b' -> on mac + * require("keyboard/hotkeys").normalize("alt-d-shift", "-"); + * // 'alt shift d' + */ +var normalize = exports.normalize = function normalize(hotkey, separator) { + if (!isString(hotkey)) + hotkey = toString(hotkey, separator); + return toString(toJSON(hotkey, separator), separator); +}; + +/* + * Utility function that splits a string of characters that defines a `hotkey` + * into modifier keys and the defining key. + * @param {String} hotkey + * @param {String} [separator=" "] + * Optional string that represents separator used to concatenate keys in the + * given `hotkey`. + * @returns {JSON} + * @examples + * + * require("keyboard/hotkeys").toJSON("accel shift b"); + * // { key: 'b', modifiers: [ 'control', 'shift' ] } -> on windows & linux + * // { key: 'b', modifiers: [ 'meta', 'shift' ] } -> on mac + * + * require("keyboard/hotkeys").normalize("alt-d-shift", "-"); + * // { key: 'd', modifiers: [ 'alt', 'shift' ] } + */ +var toJSON = exports.toJSON = function toJSON(hotkey, separator) { + separator = separator || SEPARATOR; + // Since default separator is `-`, combination may take form of `alt--`. To + // avoid misbehavior we replace `--` with `-{{SEPARATOR}}` where + // `{{SEPARATOR}}` can be swapped later. + hotkey = hotkey.toLowerCase().replace(separator + separator, separator + SWP); + + let value = {}; + let modifiers = []; + let keys = hotkey.split(separator); + keys.forEach(function(name) { + // If name is `SEPARATOR` than we swap it back. + if (name === SWP) + name = separator; + if (name in MODIFIERS) { + array.add(modifiers, MODIFIERS[name]); + } else { + if (!value.key) + value.key = name; + else + throw new TypeError(INVALID_COMBINATION); + } + }); + + if (!value.key) + throw new TypeError(INVALID_COMBINATION); + + value.modifiers = modifiers.sort(); + return value; +}; + +/** + * Utility function that takes object that defines a `hotkey` and returns + * string representation of it. + * + * _Please note that this function does not validates data neither it normalizes + * it, if you are unsure that data is well formed use `normalize` function + * instead. + * + * @param {JSON} hotkey + * @param {String} [separator=" "] + * Optional string that represents separator used to concatenate keys in the + * given `hotkey`. + * @returns {String} + * @examples + * + * require("keyboard/hotkeys").toString({ + * key: 'b', + * modifiers: [ 'control', 'shift' ] + * }, '+'); + * // 'control+shift+b + * + */ +var toString = exports.toString = function toString(hotkey, separator) { + let keys = hotkey.modifiers.slice(); + keys.push(hotkey.key); + return keys.join(separator || SEPARATOR); +}; + +/** + * Utility function takes `key` name and returns `true` if it's function key + * (F1, ..., F24) and `false` if it's not. + */ +var isFunctionKey = exports.isFunctionKey = function isFunctionKey(key) { + var $ + return key[0].toLowerCase() === 'f' && + ($ = parseInt(key.substr(1)), 0 < $ && $ < 25); +}; diff --git a/addon-sdk/source/lib/sdk/l10n.js b/addon-sdk/source/lib/sdk/l10n.js new file mode 100644 index 000000000..db5a9d7b6 --- /dev/null +++ b/addon-sdk/source/lib/sdk/l10n.js @@ -0,0 +1,91 @@ +/* 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": "stable" +}; + +const json = require("./l10n/json/core"); +const { get: getKey } = require("./l10n/core"); +const properties = require("./l10n/properties/core"); +const { getRulesForLocale } = require("./l10n/plural-rules"); + +// Retrieve the plural mapping function +var pluralMappingFunction = getRulesForLocale(json.language()) || + getRulesForLocale("en"); + +exports.get = function get(k) { + // For now, we only accept a "string" as first argument + // TODO: handle plural forms in gettext pattern + if (typeof k !== "string") + throw new Error("First argument of localization method should be a string"); + let n = arguments[1]; + + // Get translation from big hashmap or default to hard coded string: + let localized = getKey(k, n) || k; + + // # Simplest usecase: + // // String hard coded in source code: + // _("Hello world") + // // Identifier of a key stored in properties file + // _("helloString") + if (arguments.length <= 1) + return localized; + + let args = Array.slice(arguments); + let placeholders = [null, ...args.slice(typeof(n) === "number" ? 2 : 1)]; + + if (typeof localized == "object" && "other" in localized) { + // # Plural form: + // // Strings hard coded in source code: + // _(["One download", "%d downloads"], 10); + // // Identifier of a key stored in properties file + // _("downloadNumber", 0); + let n = arguments[1]; + + // First handle simple universal forms that may not be mandatory + // for each language, (i.e. not different than 'other' form, + // but still usefull for better phrasing) + // For example 0 in english is the same form than 'other' + // but we accept 'zero' form if specified in localization file + if (n === 0 && "zero" in localized) + localized = localized["zero"]; + else if (n === 1 && "one" in localized) + localized = localized["one"]; + else if (n === 2 && "two" in localized) + localized = localized["two"]; + else { + let pluralForm = pluralMappingFunction(n); + if (pluralForm in localized) + localized = localized[pluralForm]; + else // Fallback in case of error: missing plural form + localized = localized["other"]; + } + + // Simulate a string with one placeholder: + args = [null, n]; + } + + // # String with placeholders: + // // Strings hard coded in source code: + // _("Hello %s", username) + // // Identifier of a key stored in properties file + // _("helloString", username) + // * We supports `%1s`, `%2s`, ... pattern in order to change arguments order + // in translation. + // * In case of plural form, we has `%d` instead of `%s`. + let offset = 1; + if (placeholders.length > 1) { + args = placeholders; + } + + localized = localized.replace(/%(\d*)[sd]/g, (v, n) => { + let rv = args[n != "" ? n : offset]; + offset++; + return rv; + }); + + return localized; +} diff --git a/addon-sdk/source/lib/sdk/l10n/core.js b/addon-sdk/source/lib/sdk/l10n/core.js new file mode 100644 index 000000000..2f8f84c04 --- /dev/null +++ b/addon-sdk/source/lib/sdk/l10n/core.js @@ -0,0 +1,9 @@ +/* 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 json = require("./json/core"); +const properties = require("./properties/core"); + +exports.get = json.usingJSON ? json.get : properties.get; diff --git a/addon-sdk/source/lib/sdk/l10n/html.js b/addon-sdk/source/lib/sdk/l10n/html.js new file mode 100644 index 000000000..fa2cf9cf0 --- /dev/null +++ b/addon-sdk/source/lib/sdk/l10n/html.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"; + +module.metadata = { + "stability": "unstable" +}; + +const { processes, remoteRequire } = require("../remote/parent"); +remoteRequire("sdk/content/l10n-html"); + +var enabled = false; +function enable() { + if (!enabled) { + processes.port.emit("sdk/l10n/html/enable"); + enabled = true; + } +} +exports.enable = enable; + +function disable() { + if (enabled) { + processes.port.emit("sdk/l10n/html/disable"); + enabled = false; + } +} +exports.disable = disable; + +processes.forEvery(process => { + process.port.emit(enabled ? "sdk/l10n/html/enable" : "sdk/l10n/html/disable"); +}); diff --git a/addon-sdk/source/lib/sdk/l10n/json/core.js b/addon-sdk/source/lib/sdk/l10n/json/core.js new file mode 100644 index 000000000..af52f956f --- /dev/null +++ b/addon-sdk/source/lib/sdk/l10n/json/core.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": "unstable" +}; + +var usingJSON = false; +var hash = {}, bestMatchingLocale = null; +try { + let data = require("@l10n/data"); + hash = data.hash; + bestMatchingLocale = data.bestMatchingLocale; + usingJSON = true; +} +catch(e) {} + +exports.usingJSON = usingJSON; + +// Returns the translation for a given key, if available. +exports.get = function get(k) { + return k in hash ? hash[k] : null; +} + +// Returns the full length locale code: ja-JP-mac, en-US or fr +exports.locale = function locale() { + return bestMatchingLocale; +} + +// Returns the short locale code: ja, en, fr +exports.language = function language() { + return bestMatchingLocale ? bestMatchingLocale.split("-")[0].toLowerCase() + : "en"; +} diff --git a/addon-sdk/source/lib/sdk/l10n/loader.js b/addon-sdk/source/lib/sdk/l10n/loader.js new file mode 100644 index 000000000..60e219e44 --- /dev/null +++ b/addon-sdk/source/lib/sdk/l10n/loader.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"; + +module.metadata = { + "stability": "unstable" +}; + +const { Cc, Ci } = require("chrome"); +const { getPreferedLocales, findClosestLocale } = require("./locale"); +const { readURI } = require("../net/url"); +const { resolve } = require("../core/promise"); + +function parseJsonURI(uri) { + return readURI(uri). + then(JSON.parse). + then(null, function (error) { + throw Error("Failed to parse locale file:\n" + uri + "\n" + error); + }); +} + +// Returns the array stored in `locales.json` manifest that list available +// locales files +function getAvailableLocales(rootURI) { + let uri = rootURI + "locales.json"; + return parseJsonURI(uri).then(function (manifest) { + return "locales" in manifest && + Array.isArray(manifest.locales) ? + manifest.locales : []; + }); +} + +// Returns URI of the best locales file to use from the XPI +function getBestLocale(rootURI) { + // Read localization manifest file that contains list of available languages + return getAvailableLocales(rootURI).then(function (availableLocales) { + // Retrieve list of prefered locales to use + let preferedLocales = getPreferedLocales(); + + // Compute the most preferable locale to use by using these two lists + return findClosestLocale(availableLocales, preferedLocales); + }); +} + +/** + * Read localization files and returns a promise of data to put in `@l10n/data` + * pseudo module, in order to allow l10n/json/core to fetch it. + */ +exports.load = function load(rootURI) { + // First, search for a locale file: + return getBestLocale(rootURI).then(function (bestMatchingLocale) { + // It may be null if the addon doesn't have any locale file + if (!bestMatchingLocale) + return resolve(null); + + let localeURI = rootURI + "locale/" + bestMatchingLocale + ".json"; + + // Locale files only contains one big JSON object that is used as + // an hashtable of: "key to translate" => "translated key" + // TODO: We are likely to change this in order to be able to overload + // a specific key translation. For a specific package, module or line? + return parseJsonURI(localeURI).then(function (json) { + return { + hash: json, + bestMatchingLocale: bestMatchingLocale + }; + }); + }); +} diff --git a/addon-sdk/source/lib/sdk/l10n/locale.js b/addon-sdk/source/lib/sdk/l10n/locale.js new file mode 100644 index 000000000..950b33b20 --- /dev/null +++ b/addon-sdk/source/lib/sdk/l10n/locale.js @@ -0,0 +1,127 @@ +/* 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 prefs = require("../preferences/service"); +const { Cu, Cc, Ci } = require("chrome"); +const { Services } = Cu.import("resource://gre/modules/Services.jsm"); + +/** + * Gets the currently selected locale for display. + * Gets all usable locale that we can use sorted by priority of relevance + * @return Array of locales, begins with highest priority + */ +const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; +const PREF_SELECTED_LOCALE = "general.useragent.locale"; +const PREF_ACCEPT_LANGUAGES = "intl.accept_languages"; + +function getPreferedLocales(caseSensitve) { + let locales = []; + function addLocale(locale) { + locale = locale.trim(); + if (!caseSensitve) + locale = locale.toLowerCase(); + if (locales.indexOf(locale) === -1) + locales.push(locale); + } + + // Most important locale is OS one. But we use it, only if + // "intl.locale.matchOS" pref is set to `true`. + // Currently only used for multi-locales mobile builds. + // http://mxr.mozilla.org/mozilla-central/source/mobile/android/installer/Makefile.in#46 + if (prefs.get(PREF_MATCH_OS_LOCALE, false)) { + let localeService = Cc["@mozilla.org/intl/nslocaleservice;1"]. + getService(Ci.nsILocaleService); + let osLocale = localeService.getLocaleComponentForUserAgent(); + addLocale(osLocale); + } + + // In some cases, mainly on Fennec and on Linux version, + // `general.useragent.locale` is a special 'localized' value, like: + // "chrome://global/locale/intl.properties" + let browserUiLocale = prefs.getLocalized(PREF_SELECTED_LOCALE, "") || + prefs.get(PREF_SELECTED_LOCALE, ""); + if (browserUiLocale) + addLocale(browserUiLocale); + + // Third priority is the list of locales used for web content + 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 + for (let locale of contentLocales.split(",")) + addLocale(locale.replace(/(^\s+)|(\s+$)/g, "")); + } + + // Finally, we ensure that en-US is the final fallback if it wasn't added + addLocale("en-US"); + + return locales; +} +exports.getPreferedLocales = getPreferedLocales; + +/** + * Selects the closest matching locale from a list of locales. + * + * @param aLocales + * An array of available locales + * @param aMatchLocales + * An array of prefered locales, ordered by priority. Most wanted first. + * Locales have to be in lowercase. + * If null, uses getPreferedLocales() results + * @return the best match for the currently selected locale + * + * Stolen from http://dxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm + */ +exports.findClosestLocale = function findClosestLocale(aLocales, aMatchLocales) { + aMatchLocales = aMatchLocales || getPreferedLocales(); + + // Holds the best matching localized resource + let bestmatch = null; + // The number of locale parts it matched with + let bestmatchcount = 0; + // The number of locale parts in the match + let bestpartcount = 0; + + for (let locale of aMatchLocales) { + let lparts = locale.split("-"); + for (let localized of aLocales) { + let found = localized.toLowerCase(); + // Exact match is returned immediately + if (locale == found) + return localized; + + let fparts = found.split("-"); + /* If we have found a possible match and this one isn't any longer + then we dont need to check further. */ + if (bestmatch && fparts.length < bestmatchcount) + continue; + + // Count the number of parts that match + let maxmatchcount = Math.min(fparts.length, lparts.length); + let matchcount = 0; + while (matchcount < maxmatchcount && + fparts[matchcount] == lparts[matchcount]) + matchcount++; + + /* If we matched more than the last best match or matched the same and + this locale is less specific than the last best match. */ + if (matchcount > bestmatchcount || + (matchcount == bestmatchcount && fparts.length < bestpartcount)) { + bestmatch = localized; + bestmatchcount = matchcount; + bestpartcount = fparts.length; + } + } + // If we found a valid match for this locale return it + if (bestmatch) + return bestmatch; + } + return null; +} diff --git a/addon-sdk/source/lib/sdk/l10n/plural-rules.js b/addon-sdk/source/lib/sdk/l10n/plural-rules.js new file mode 100644 index 000000000..a3ef48a5e --- /dev/null +++ b/addon-sdk/source/lib/sdk/l10n/plural-rules.js @@ -0,0 +1,407 @@ +/* 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/. */ + +// This file is automatically generated with /python-lib/plural-rules-generator.py +// Fetching data from: http://unicode.org/repos/cldr/trunk/common/supplemental/plurals.xml + +// Mapping of short locale name == to == > rule index in following list + +module.metadata = { + "stability": "unstable" +}; + +const LOCALES_TO_RULES = { + "af": 3, + "ak": 4, + "am": 4, + "ar": 1, + "asa": 3, + "az": 0, + "be": 11, + "bem": 3, + "bez": 3, + "bg": 3, + "bh": 4, + "bm": 0, + "bn": 3, + "bo": 0, + "br": 20, + "brx": 3, + "bs": 11, + "ca": 3, + "cgg": 3, + "chr": 3, + "cs": 12, + "cy": 17, + "da": 3, + "de": 3, + "dv": 3, + "dz": 0, + "ee": 3, + "el": 3, + "en": 3, + "eo": 3, + "es": 3, + "et": 3, + "eu": 3, + "fa": 0, + "ff": 5, + "fi": 3, + "fil": 4, + "fo": 3, + "fr": 5, + "fur": 3, + "fy": 3, + "ga": 8, + "gd": 24, + "gl": 3, + "gsw": 3, + "gu": 3, + "guw": 4, + "gv": 23, + "ha": 3, + "haw": 3, + "he": 2, + "hi": 4, + "hr": 11, + "hu": 0, + "id": 0, + "ig": 0, + "ii": 0, + "is": 3, + "it": 3, + "iu": 7, + "ja": 0, + "jmc": 3, + "jv": 0, + "ka": 0, + "kab": 5, + "kaj": 3, + "kcg": 3, + "kde": 0, + "kea": 0, + "kk": 3, + "kl": 3, + "km": 0, + "kn": 0, + "ko": 0, + "ksb": 3, + "ksh": 21, + "ku": 3, + "kw": 7, + "lag": 18, + "lb": 3, + "lg": 3, + "ln": 4, + "lo": 0, + "lt": 10, + "lv": 6, + "mas": 3, + "mg": 4, + "mk": 16, + "ml": 3, + "mn": 3, + "mo": 9, + "mr": 3, + "ms": 0, + "mt": 15, + "my": 0, + "nah": 3, + "naq": 7, + "nb": 3, + "nd": 3, + "ne": 3, + "nl": 3, + "nn": 3, + "no": 3, + "nr": 3, + "nso": 4, + "ny": 3, + "nyn": 3, + "om": 3, + "or": 3, + "pa": 3, + "pap": 3, + "pl": 13, + "ps": 3, + "pt": 3, + "rm": 3, + "ro": 9, + "rof": 3, + "ru": 11, + "rwk": 3, + "sah": 0, + "saq": 3, + "se": 7, + "seh": 3, + "ses": 0, + "sg": 0, + "sh": 11, + "shi": 19, + "sk": 12, + "sl": 14, + "sma": 7, + "smi": 7, + "smj": 7, + "smn": 7, + "sms": 7, + "sn": 3, + "so": 3, + "sq": 3, + "sr": 11, + "ss": 3, + "ssy": 3, + "st": 3, + "sv": 3, + "sw": 3, + "syr": 3, + "ta": 3, + "te": 3, + "teo": 3, + "th": 0, + "ti": 4, + "tig": 3, + "tk": 3, + "tl": 4, + "tn": 3, + "to": 0, + "tr": 0, + "ts": 3, + "tzm": 22, + "uk": 11, + "ur": 3, + "ve": 3, + "vi": 0, + "vun": 3, + "wa": 4, + "wae": 3, + "wo": 0, + "xh": 3, + "xog": 3, + "yo": 0, + "zh": 0, + "zu": 3 +}; + +// Utility functions for plural rules methods +function isIn(n, list) { + return list.indexOf(n) !== -1; +} +function isBetween(n, start, end) { + return start <= n && n <= end; +} + +// List of all plural rules methods, that maps an integer to the plural form name to use +const RULES = { + "0": function (n) { + + return "other" + }, + "1": function (n) { + if ((isBetween((n % 100), 3, 10))) + return "few"; + if (n == 0) + return "zero"; + if ((isBetween((n % 100), 11, 99))) + return "many"; + if (n == 2) + return "two"; + if (n == 1) + return "one"; + return "other" + }, + "2": function (n) { + if (n != 0 && (n % 10) == 0) + return "many"; + if (n == 2) + return "two"; + if (n == 1) + return "one"; + return "other" + }, + "3": function (n) { + if (n == 1) + return "one"; + return "other" + }, + "4": function (n) { + if ((isBetween(n, 0, 1))) + return "one"; + return "other" + }, + "5": function (n) { + if ((isBetween(n, 0, 2)) && n != 2) + return "one"; + return "other" + }, + "6": function (n) { + if (n == 0) + return "zero"; + if ((n % 10) == 1 && (n % 100) != 11) + return "one"; + return "other" + }, + "7": function (n) { + if (n == 2) + return "two"; + if (n == 1) + return "one"; + return "other" + }, + "8": function (n) { + if ((isBetween(n, 3, 6))) + return "few"; + if ((isBetween(n, 7, 10))) + return "many"; + if (n == 2) + return "two"; + if (n == 1) + return "one"; + return "other" + }, + "9": function (n) { + if (n == 0 || n != 1 && (isBetween((n % 100), 1, 19))) + return "few"; + if (n == 1) + return "one"; + return "other" + }, + "10": function (n) { + if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) + return "few"; + if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19))) + return "one"; + return "other" + }, + "11": function (n) { + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) + return "few"; + if ((n % 10) == 0 || (isBetween((n % 10), 5, 9)) || (isBetween((n % 100), 11, 14))) + return "many"; + if ((n % 10) == 1 && (n % 100) != 11) + return "one"; + return "other" + }, + "12": function (n) { + if ((isBetween(n, 2, 4))) + return "few"; + if (n == 1) + return "one"; + return "other" + }, + "13": function (n) { + if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) + return "few"; + if (n != 1 && (isBetween((n % 10), 0, 1)) || (isBetween((n % 10), 5, 9)) || (isBetween((n % 100), 12, 14))) + return "many"; + if (n == 1) + return "one"; + return "other" + }, + "14": function (n) { + if ((isBetween((n % 100), 3, 4))) + return "few"; + if ((n % 100) == 2) + return "two"; + if ((n % 100) == 1) + return "one"; + return "other" + }, + "15": function (n) { + if (n == 0 || (isBetween((n % 100), 2, 10))) + return "few"; + if ((isBetween((n % 100), 11, 19))) + return "many"; + if (n == 1) + return "one"; + return "other" + }, + "16": function (n) { + if ((n % 10) == 1 && n != 11) + return "one"; + return "other" + }, + "17": function (n) { + if (n == 3) + return "few"; + if (n == 0) + return "zero"; + if (n == 6) + return "many"; + if (n == 2) + return "two"; + if (n == 1) + return "one"; + return "other" + }, + "18": function (n) { + if (n == 0) + return "zero"; + if ((isBetween(n, 0, 2)) && n != 0 && n != 2) + return "one"; + return "other" + }, + "19": function (n) { + if ((isBetween(n, 2, 10))) + return "few"; + if ((isBetween(n, 0, 1))) + return "one"; + return "other" + }, + "20": function (n) { + if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(isBetween((n % 100), 10, 19) || isBetween((n % 100), 70, 79) || isBetween((n % 100), 90, 99))) + return "few"; + if ((n % 1000000) == 0 && n != 0) + return "many"; + if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92])) + return "two"; + if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91])) + return "one"; + return "other" + }, + "21": function (n) { + if (n == 0) + return "zero"; + if (n == 1) + return "one"; + return "other" + }, + "22": function (n) { + if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) + return "one"; + return "other" + }, + "23": function (n) { + if ((isBetween((n % 10), 1, 2)) || (n % 20) == 0) + return "one"; + return "other" + }, + "24": function (n) { + if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) + return "few"; + if (isIn(n, [2, 12])) + return "two"; + if (isIn(n, [1, 11])) + return "one"; + return "other" + }, +}; + +/** + * Return a function that gives the plural form name for a given integer + * for the specified `locale` + * let fun = getRulesForLocale('en'); + * fun(1) -> 'one' + * fun(0) -> 'other' + * fun(1000) -> 'other' + */ +exports.getRulesForLocale = function getRulesForLocale(locale) { + let index = LOCALES_TO_RULES[locale]; + if (!(index in RULES)) { + console.warn('Plural form unknown for locale "' + locale + '"'); + return function () { return "other"; }; + } + return RULES[index]; +} + diff --git a/addon-sdk/source/lib/sdk/l10n/prefs.js b/addon-sdk/source/lib/sdk/l10n/prefs.js new file mode 100644 index 000000000..8ee26fc5b --- /dev/null +++ b/addon-sdk/source/lib/sdk/l10n/prefs.js @@ -0,0 +1,51 @@ +/* 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 } = require("../system/events"); +const core = require("./core"); +const { id: jetpackId } = require('../self'); + +const OPTIONS_DISPLAYED = "addon-options-displayed"; + +function enable() { + on(OPTIONS_DISPLAYED, onOptionsDisplayed); +} +exports.enable = enable; + +function onOptionsDisplayed({ subject: document, data: addonId }) { + if (addonId !== jetpackId) + return; + localizeInlineOptions(document); +} + +function localizeInlineOptions(document) { + let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' + + 'button[data-jetpack-id="' + jetpackId + '"][pref-name]'; + let nodes = document.querySelectorAll(query); + for (let node of nodes) { + let name = node.getAttribute("pref-name"); + if (node.tagName == "setting") { + let desc = core.get(name + "_description"); + if (desc) + node.setAttribute("desc", desc); + let title = core.get(name + "_title"); + if (title) + node.setAttribute("title", title); + + for (let item of node.querySelectorAll("menuitem, radio")) { + let key = name + "_options." + item.getAttribute("label"); + let label = core.get(key); + if (label) + item.setAttribute("label", label); + } + } + else if (node.tagName == "button") { + let label = core.get(name + "_label"); + if (label) + node.setAttribute("label", label); + } + } +} +exports.localizeInlineOptions = localizeInlineOptions; diff --git a/addon-sdk/source/lib/sdk/l10n/properties/core.js b/addon-sdk/source/lib/sdk/l10n/properties/core.js new file mode 100644 index 000000000..7a9081d0b --- /dev/null +++ b/addon-sdk/source/lib/sdk/l10n/properties/core.js @@ -0,0 +1,87 @@ +/* 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 { newURI } = require('../../url/utils') +const { getRulesForLocale } = require("../plural-rules"); +const { getPreferedLocales } = require('../locale'); +const { rootURI } = require("@loader/options"); +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +const baseURI = rootURI + "locale/"; +const preferedLocales = getPreferedLocales(true); + +// Make sure we don't get stale data after an update +// (See Bug 1300735 for rationale). +Services.strings.flushBundles(); + +function getLocaleURL(locale) { + // if the locale is a valid chrome URI, return it + try { + let uri = newURI(locale); + if (uri.scheme == 'chrome') + return uri.spec; + } + catch(_) {} + // otherwise try to construct the url + return baseURI + locale + ".properties"; +} + +function getKey(locale, key) { + let bundle = Services.strings.createBundle(getLocaleURL(locale)); + try { + return bundle.GetStringFromName(key) + ""; + } + catch (_) {} + return undefined; +} + +function get(key, n, locales) { + // try this locale + let locale = locales.shift(); + let localized; + + if (typeof n == 'number') { + if (n == 0) { + localized = getKey(locale, key + '[zero]'); + } + else if (n == 1) { + localized = getKey(locale, key + '[one]'); + } + else if (n == 2) { + localized = getKey(locale, key + '[two]'); + } + + if (!localized) { + // Retrieve the plural mapping function + let pluralForm = (getRulesForLocale(locale.split("-")[0].toLowerCase()) || + getRulesForLocale("en"))(n); + localized = getKey(locale, key + '[' + pluralForm + ']'); + } + + if (!localized) { + localized = getKey(locale, key + '[other]'); + } + } + + if (!localized) { + localized = getKey(locale, key); + } + + if (!localized) { + localized = getKey(locale, key + '[other]'); + } + + if (localized) { + return localized; + } + + // try next locale + if (locales.length) + return get(key, n, locales); + + return undefined; +} +exports.get = (k, n) => get(k, n, Array.slice(preferedLocales)); diff --git a/addon-sdk/source/lib/sdk/lang/functional.js b/addon-sdk/source/lib/sdk/lang/functional.js new file mode 100644 index 000000000..66e30edfa --- /dev/null +++ b/addon-sdk/source/lib/sdk/lang/functional.js @@ -0,0 +1,47 @@ +/* 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/. */ + +// Disclaimer: Some of the functions in this module implement APIs from +// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for +// those goes to him. + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { defer, remit, delay, debounce, + throttle } = require("./functional/concurrent"); +const { method, invoke, partial, curry, compose, wrap, identity, memoize, once, + cache, complement, constant, when, apply, flip, field, query, + isInstance, chainable, is, isnt } = require("./functional/core"); + +exports.defer = defer; +exports.remit = remit; +exports.delay = delay; +exports.debounce = debounce; +exports.throttle = throttle; + +exports.method = method; +exports.invoke = invoke; +exports.partial = partial; +exports.curry = curry; +exports.compose = compose; +exports.wrap = wrap; +exports.identity = identity; +exports.memoize = memoize; +exports.once = once; +exports.cache = cache; +exports.complement = complement; +exports.constant = constant; +exports.when = when; +exports.apply = apply; +exports.flip = flip; +exports.field = field; +exports.query = query; +exports.isInstance = isInstance; +exports.chainable = chainable; +exports.is = is; +exports.isnt = isnt; diff --git a/addon-sdk/source/lib/sdk/lang/functional/concurrent.js b/addon-sdk/source/lib/sdk/lang/functional/concurrent.js new file mode 100644 index 000000000..85e8cff46 --- /dev/null +++ b/addon-sdk/source/lib/sdk/lang/functional/concurrent.js @@ -0,0 +1,110 @@ +/* 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/. */ + +// Disclaimer: Some of the functions in this module implement APIs from +// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for +// those goes to him. + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { arity, name, derive, invoke } = require("./helpers"); +const { setTimeout, clearTimeout, setImmediate } = require("../../timers"); + +/** + * Takes a function and returns a wrapped one instead, calling which will call + * original function in the next turn of event loop. This is basically utility + * to do `setImmediate(function() { ... })`, with a difference that returned + * function is reused, instead of creating a new one each time. This also allows + * to use this functions as event listeners. + */ +const defer = f => derive(function(...args) { + setImmediate(invoke, f, args, this); +}, f); +exports.defer = defer; +// Exporting `remit` alias as `defer` may conflict with promises. +exports.remit = defer; + +/** + * Much like setTimeout, invokes function after wait milliseconds. If you pass + * the optional arguments, they will be forwarded on to the function when it is + * invoked. + */ +const delay = function delay(f, ms, ...args) { + setTimeout(() => f.apply(this, args), ms); +}; +exports.delay = delay; + +/** + * From underscore's `_.debounce` + * http://underscorejs.org + * (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Underscore may be freely distributed under the MIT license. + */ +const debounce = function debounce (fn, wait) { + let timeout, args, context, timestamp, result; + + let later = function () { + let last = Date.now() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + result = fn.apply(context, args); + context = args = null; + } + }; + + return function (...aArgs) { + context = this; + args = aArgs; + timestamp = Date.now(); + if (!timeout) { + timeout = setTimeout(later, wait); + } + + return result; + }; +}; +exports.debounce = debounce; + +/** + * From underscore's `_.throttle` + * http://underscorejs.org + * (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Underscore may be freely distributed under the MIT license. + */ +const throttle = function throttle (func, wait, options) { + let context, args, result; + let timeout = null; + let previous = 0; + options || (options = {}); + let later = function() { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + result = func.apply(context, args); + context = args = null; + }; + return function() { + let now = Date.now(); + if (!previous && options.leading === false) previous = now; + let remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; +}; +exports.throttle = throttle; diff --git a/addon-sdk/source/lib/sdk/lang/functional/core.js b/addon-sdk/source/lib/sdk/lang/functional/core.js new file mode 100644 index 000000000..0d9143364 --- /dev/null +++ b/addon-sdk/source/lib/sdk/lang/functional/core.js @@ -0,0 +1,290 @@ +/* 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/. */ + +// Disclaimer: Some of the functions in this module implement APIs from +// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for +// those goes to him. + +"use strict"; + +module.metadata = { + "stability": "unstable" +} +const { arity, name, derive, invoke } = require("./helpers"); + +/** + * Takes variadic numeber of functions and returns composed one. + * Returned function pushes `this` pseudo-variable to the head + * of the passed arguments and invokes all the functions from + * left to right passing same arguments to them. Composite function + * returns return value of the right most funciton. + */ +const method = (...lambdas) => { + return function method(...args) { + args.unshift(this); + return lambdas.reduce((_, lambda) => lambda.apply(this, args), + void(0)); + }; +}; +exports.method = method; + +/** + * Invokes `callee` by passing `params` as an arguments and `self` as `this` + * pseudo-variable. Returns value that is returned by a callee. + * @param {Function} callee + * Function to invoke. + * @param {Array} params + * Arguments to invoke function with. + * @param {Object} self + * Object to be passed as a `this` pseudo variable. + */ +exports.invoke = invoke; + +/** + * Takes a function and bind values to one or more arguments, returning a new + * function of smaller arity. + * + * @param {Function} fn + * The function to partial + * + * @returns The new function with binded values + */ +const partial = (f, ...curried) => { + if (typeof(f) !== "function") + throw new TypeError(String(f) + " is not a function"); + + let fn = derive(function(...args) { + return f.apply(this, curried.concat(args)); + }, f); + fn.arity = arity(f) - curried.length; + return fn; +}; +exports.partial = partial; + +/** + * Returns function with implicit currying, which will continue currying until + * expected number of argument is collected. Expected number of arguments is + * determined by `fn.length`. Using this with variadic functions is stupid, + * so don't do it. + * + * @examples + * + * var sum = curry(function(a, b) { + * return a + b + * }) + * console.log(sum(2, 2)) // 4 + * console.log(sum(2)(4)) // 6 + */ +const curry = new function() { + const currier = (fn, arity, params) => { + // Function either continues to curry arguments or executes function + // if desired arguments have being collected. + const curried = function(...input) { + // Prepend all curried arguments to the given arguments. + if (params) input.unshift.apply(input, params); + // If expected number of arguments has being collected invoke fn, + // othrewise return curried version Otherwise continue curried. + return (input.length >= arity) ? fn.apply(this, input) : + currier(fn, arity, input); + }; + curried.arity = arity - (params ? params.length : 0); + + return curried; + }; + + return fn => currier(fn, arity(fn)); +}; +exports.curry = curry; + +/** + * Returns the composition of a list of functions, where each function consumes + * the return value of the function that follows. In math terms, composing the + * functions `f()`, `g()`, and `h()` produces `f(g(h()))`. + * @example + * + * var greet = function(name) { return "hi: " + name; }; + * var exclaim = function(statement) { return statement + "!"; }; + * var welcome = compose(exclaim, greet); + * + * welcome('moe'); // => 'hi: moe!' + */ +function compose(...lambdas) { + return function composed(...args) { + let index = lambdas.length; + while (0 <= --index) + args = [lambdas[index].apply(this, args)]; + + return args[0]; + }; +} +exports.compose = compose; + +/* + * Returns the first function passed as an argument to the second, + * allowing you to adjust arguments, run code before and after, and + * conditionally execute the original function. + * @example + * + * var hello = function(name) { return "hello: " + name; }; + * hello = wrap(hello, function(f) { + * return "before, " + f("moe") + ", after"; + * }); + * + * hello(); // => 'before, hello: moe, after' + */ +const wrap = (f, wrapper) => derive(function wrapped(...args) { + return wrapper.apply(this, [f].concat(args)); +}, f); +exports.wrap = wrap; + +/** + * Returns the same value that is used as the argument. In math: f(x) = x + */ +const identity = value => value; +exports.identity = identity; + +/** + * Memoizes a given function by caching the computed result. Useful for + * speeding up slow-running computations. If passed an optional hashFunction, + * it will be used to compute the hash key for storing the result, based on + * the arguments to the original function. The default hashFunction just uses + * the first argument to the memoized function as the key. + */ +const memoize = (f, hasher) => { + let memo = Object.create(null); + let cache = new WeakMap(); + hasher = hasher || identity; + return derive(function memoizer(...args) { + const key = hasher.apply(this, args); + const type = typeof(key); + if (key && (type === "object" || type === "function")) { + if (!cache.has(key)) + cache.set(key, f.apply(this, args)); + return cache.get(key); + } + else { + if (!(key in memo)) + memo[key] = f.apply(this, args); + return memo[key]; + } + }, f); +}; +exports.memoize = memoize; + +/* + * Creates a version of the function that can only be called one time. Repeated + * calls to the modified function will have no effect, returning the value from + * the original call. Useful for initialization functions, instead of having to + * set a boolean flag and then check it later. + */ +const once = f => { + let ran = false, cache; + return derive(function(...args) { + return ran ? cache : (ran = true, cache = f.apply(this, args)); + }, f); +}; +exports.once = once; +// export cache as once will may be conflicting with event once a lot. +exports.cache = once; + +// Takes a `f` function and returns a function that takes the same +// arguments as `f`, has the same effects, if any, and returns the +// opposite truth value. +const complement = f => derive(function(...args) { + return args.length < arity(f) ? complement(partial(f, ...args)) : + !f.apply(this, args); +}, f); +exports.complement = complement; + +// Constructs function that returns `x` no matter what is it +// invoked with. +const constant = x => _ => x; +exports.constant = constant; + +// Takes `p` predicate, `consequent` function and an optional +// `alternate` function and composes function that returns +// application of arguments over `consequent` if application over +// `p` is `true` otherwise returns application over `alternate`. +// If `alternate` is not a function returns `undefined`. +const when = (p, consequent, alternate) => { + if (typeof(alternate) !== "function" && alternate !== void(0)) + throw TypeError("alternate must be a function"); + if (typeof(consequent) !== "function") + throw TypeError("consequent must be a function"); + + return function(...args) { + return p.apply(this, args) ? + consequent.apply(this, args) : + alternate && alternate.apply(this, args); + }; +}; +exports.when = when; + +// Apply function that behaves as `apply` does in lisp: +// apply(f, x, [y, z]) => f.apply(f, [x, y, z]) +// apply(f, x) => f.apply(f, [x]) +const apply = (f, ...rest) => f.apply(f, rest.concat(rest.pop())); +exports.apply = apply; + +// Returns function identical to given `f` but with flipped order +// of arguments. +const flip = f => derive(function(...args) { + return f.apply(this, args.reverse()); +}, f); +exports.flip = flip; + +// Takes field `name` and `target` and returns value of that field. +// If `target` is `null` or `undefined` it would be returned back +// instead of attempt to access it's field. Function is implicitly +// curried, this allows accessor function generation by calling it +// with only `name` argument. +const field = curry((name, target) => + // Note: Permisive `==` is intentional. + target == null ? target : target[name]); +exports.field = field; + +// Takes `.` delimited string representing `path` to a nested field +// and a `target` to get it from. For convinience function is +// implicitly curried, there for accessors can be created by invoking +// it with just a `path` argument. +const query = curry((path, target) => { + const names = path.split("."); + const count = names.length; + let index = 0; + let result = target; + // Note: Permisive `!=` is intentional. + while (result != null && index < count) { + result = result[names[index]]; + index = index + 1; + } + return result; +}); +exports.query = query; + +// Takes `Type` (constructor function) and a `value` and returns +// `true` if `value` is instance of the given `Type`. Function is +// implicitly curried this allows predicate generation by calling +// function with just first argument. +const isInstance = curry((Type, value) => value instanceof Type); +exports.isInstance = isInstance; + +/* + * Takes a funtion and returns a wrapped function that returns `this` + */ +const chainable = f => derive(function(...args) { + f.apply(this, args); + return this; +}, f); +exports.chainable = chainable; + +// Functions takes `expected` and `actual` values and returns `true` if +// `expected === actual`. Returns curried function if called with less then +// two arguments. +// +// [ 1, 0, 1, 0, 1 ].map(is(1)) // => [ true, false, true, false, true ] +const is = curry((expected, actual) => actual === expected); +exports.is = is; + +const isnt = complement(is); +exports.isnt = isnt; diff --git a/addon-sdk/source/lib/sdk/lang/functional/helpers.js b/addon-sdk/source/lib/sdk/lang/functional/helpers.js new file mode 100644 index 000000000..60f4e3300 --- /dev/null +++ b/addon-sdk/source/lib/sdk/lang/functional/helpers.js @@ -0,0 +1,29 @@ +/* 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/. */ + +// Disclaimer: Some of the functions in this module implement APIs from +// Jeremy Ashkenas's http://underscorejs.org/ library and all credits for +// those goes to him. + +"use strict"; + +module.metadata = { + "stability": "unstable" +} + +const arity = f => f.arity || f.length; +exports.arity = arity; + +const name = f => f.displayName || f.name; +exports.name = name; + +const derive = (f, source) => { + f.displayName = name(source); + f.arity = arity(source); + return f; +}; +exports.derive = derive; + +const invoke = (callee, params, self) => callee.apply(self, params); +exports.invoke = invoke; diff --git a/addon-sdk/source/lib/sdk/lang/type.js b/addon-sdk/source/lib/sdk/lang/type.js new file mode 100644 index 000000000..b50e6be4c --- /dev/null +++ b/addon-sdk/source/lib/sdk/lang/type.js @@ -0,0 +1,388 @@ +/* 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" +}; + +/** + * Returns `true` if `value` is `undefined`. + * @examples + * var foo; isUndefined(foo); // true + * isUndefined(0); // false + */ +function isUndefined(value) { + return value === undefined; +} +exports.isUndefined = isUndefined; + +/** + * Returns `true` if value is `null`. + * @examples + * isNull(null); // true + * isNull(undefined); // false + */ +function isNull(value) { + return value === null; +} +exports.isNull = isNull; + +/** + * Returns `true` if value is `null` or `undefined`. + * It's equivalent to `== null`, but resolve the ambiguity of the writer + * intention, makes clear that he's clearly checking both `null` and `undefined` + * values, and it's not a typo for `=== null`. + */ +function isNil(value) { + return value === null || value === undefined; +} +exports.isNil = isNil; + +function isBoolean(value) { + return typeof value === "boolean"; +} +exports.isBoolean = isBoolean; +/** + * Returns `true` if value is a string. + * @examples + * isString("moe"); // true + */ +function isString(value) { + return typeof value === "string"; +} +exports.isString = isString; + +/** + * Returns `true` if `value` is a number. + * @examples + * isNumber(8.4 * 5); // true + */ +function isNumber(value) { + return typeof value === "number"; +} +exports.isNumber = isNumber; + +/** + * Returns `true` if `value` is a `RegExp`. + * @examples + * isRegExp(/moe/); // true + */ +function isRegExp(value) { + return isObject(value) && instanceOf(value, RegExp); +} +exports.isRegExp = isRegExp; + +/** + * Returns true if `value` is a `Date`. + * @examples + * isDate(new Date()); // true + */ +function isDate(value) { + return isObject(value) && instanceOf(value, Date); +} +exports.isDate = isDate; + +/** + * Returns true if object is a Function. + * @examples + * isFunction(function foo(){}) // true + */ +function isFunction(value) { + return typeof value === "function"; +} +exports.isFunction = isFunction; + +/** + * Returns `true` if `value` is an object (please note that `null` is considered + * to be an atom and not an object). + * @examples + * isObject({}) // true + * isObject(null) // false + */ +function isObject(value) { + return typeof value === "object" && value !== null; +} +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 + * isArray([1, 2, 3]) // true + * isArray({ 0: 'foo', length: 1 }) // false + */ +var isArray = Array.isArray; +exports.isArray = isArray; + +/** + * Returns `true` if `value` is an Arguments object. + * @examples + * (function(){ return isArguments(arguments); })(1, 2, 3); // true + * isArguments([1,2,3]); // false + */ +function isArguments(value) { + return Object.prototype.toString.call(value) === "[object Arguments]"; +} +exports.isArguments = isArguments; + +var isMap = value => Object.prototype.toString.call(value) === "[object Map]" +exports.isMap = isMap; + +var isSet = value => Object.prototype.toString.call(value) === "[object Set]" +exports.isSet = isSet; + +/** + * Returns true if it is a primitive `value`. (null, undefined, number, + * boolean, string) + * @examples + * isPrimitive(3) // true + * isPrimitive('foo') // true + * isPrimitive({ bar: 3 }) // false + */ +function isPrimitive(value) { + return !isFunction(value) && !isObject(value); +} +exports.isPrimitive = isPrimitive; + +/** + * Returns `true` if given `object` is flat (it is direct decedent of + * `Object.prototype` or `null`). + * @examples + * isFlat({}) // true + * isFlat(new Type()) // false + */ +function isFlat(object) { + return isObject(object) && (isNull(Object.getPrototypeOf(object)) || + isNull(Object.getPrototypeOf( + Object.getPrototypeOf(object)))); +} +exports.isFlat = isFlat; + +/** + * Returns `true` if object contains no values. + */ +function isEmpty(object) { + if (isObject(object)) { + for (var key in object) + return false; + return true; + } + return false; +} +exports.isEmpty = isEmpty; + +/** + * Returns `true` if `value` is an array / flat object containing only atomic + * values and other flat objects. + */ +function isJSON(value, visited) { + // Adding value to array of visited values. + (visited || (visited = [])).push(value); + // If `value` is an atom return `true` cause it's valid JSON. + return isPrimitive(value) || + // If `value` is an array of JSON values that has not been visited + // yet. + (isArray(value) && value.every(function(element) { + return isJSON(element, visited); + })) || + // If `value` is a plain object containing properties with a JSON + // values it's a valid JSON. + (isFlat(value) && Object.keys(value).every(function(key) { + var $ = Object.getOwnPropertyDescriptor(value, key); + // Check every proprety of a plain object to verify that + // it's neither getter nor setter, but a JSON value, that + // has not been visited yet. + return ((!isObject($.value) || !~visited.indexOf($.value)) && + !('get' in $) && !('set' in $) && + isJSON($.value, visited)); + })); +} +exports.isJSON = function (value) { + return isJSON(value); +}; + +/** + * Returns `true` if `value` is JSONable + */ +const isJSONable = (value) => { + try { + JSON.parse(JSON.stringify(value)); + } + catch (e) { + return false; + } + return true; +}; +exports.isJSONable = isJSONable; + +/** + * Returns if `value` is an instance of a given `Type`. This is exactly same as + * `value instanceof Type` with a difference that `Type` can be from a scope + * that has a different top level object. (Like in case where `Type` is a + * function from different iframe / jetpack module / sandbox). + */ +function instanceOf(value, Type) { + var isConstructorNameSame; + var isConstructorSourceSame; + + // If `instanceof` returned `true` we know result right away. + var isInstanceOf = value instanceof Type; + + // If `instanceof` returned `false` we do ducktype check since `Type` may be + // from a different sandbox. If a constructor of the `value` or a constructor + // of the value's prototype has same name and source we assume that it's an + // instance of the Type. + if (!isInstanceOf && value) { + isConstructorNameSame = value.constructor.name === Type.name; + isConstructorSourceSame = String(value.constructor) == String(Type); + isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) || + instanceOf(Object.getPrototypeOf(value), Type); + } + return isInstanceOf; +} +exports.instanceOf = instanceOf; + +/** + * Function returns textual representation of a value passed to it. Function + * takes additional `indent` argument that is used for indentation. Also + * optional `limit` argument may be passed to limit amount of detail returned. + * @param {Object} value + * @param {String} [indent=" "] + * @param {Number} [limit] + */ +function source(value, indent, limit, offset, visited) { + var result; + var names; + var nestingIndex; + var isCompact = !isUndefined(limit); + + indent = indent || " "; + offset = (offset || ""); + result = ""; + visited = visited || []; + + if (isUndefined(value)) { + result += "undefined"; + } + else if (isNull(value)) { + result += "null"; + } + else if (isString(value)) { + result += '"' + value + '"'; + } + else if (isFunction(value)) { + value = String(value).split("\n"); + if (isCompact && value.length > 2) { + value = value.splice(0, 2); + value.push("...}"); + } + result += value.join("\n" + offset); + } + else if (isArray(value)) { + if ((nestingIndex = (visited.indexOf(value) + 1))) { + result = "#" + nestingIndex + "#"; + } + else { + visited.push(value); + + if (isCompact) + value = value.slice(0, limit); + + result += "[\n"; + result += value.map(function(value) { + return offset + indent + source(value, indent, limit, offset + indent, + visited); + }).join(",\n"); + result += isCompact && value.length > limit ? + ",\n" + offset + "...]" : "\n" + offset + "]"; + } + } + else if (isObject(value)) { + if ((nestingIndex = (visited.indexOf(value) + 1))) { + result = "#" + nestingIndex + "#" + } + else { + visited.push(value) + + names = Object.keys(value); + + result += "{ // " + value + "\n"; + result += (isCompact ? names.slice(0, limit) : names).map(function(name) { + var _limit = isCompact ? limit - 1 : limit; + var descriptor = Object.getOwnPropertyDescriptor(value, name); + var result = offset + indent + "// "; + var accessor; + if (0 <= name.indexOf(" ")) + name = '"' + name + '"'; + + if (descriptor.writable) + result += "writable "; + if (descriptor.configurable) + result += "configurable "; + if (descriptor.enumerable) + result += "enumerable "; + + result += "\n"; + if ("value" in descriptor) { + result += offset + indent + name + ": "; + result += source(descriptor.value, indent, _limit, indent + offset, + visited); + } + else { + + if (descriptor.get) { + result += offset + indent + "get " + name + " "; + accessor = source(descriptor.get, indent, _limit, indent + offset, + visited); + result += accessor.substr(accessor.indexOf("{")); + } + + if (descriptor.set) { + result += offset + indent + "set " + name + " "; + accessor = source(descriptor.set, indent, _limit, indent + offset, + visited); + result += accessor.substr(accessor.indexOf("{")); + } + } + return result; + }).join(",\n"); + + if (isCompact) { + if (names.length > limit && limit > 0) { + result += ",\n" + offset + indent + "//..."; + } + } + else { + if (names.length) + result += ","; + + result += "\n" + offset + indent + '"__proto__": '; + result += source(Object.getPrototypeOf(value), indent, 0, + offset + indent); + } + + result += "\n" + offset + "}"; + } + } + else { + result += String(value); + } + return result; +} +exports.source = function (value, indentation, limit) { + return source(value, indentation, limit); +}; diff --git a/addon-sdk/source/lib/sdk/lang/weak-set.js b/addon-sdk/source/lib/sdk/lang/weak-set.js new file mode 100644 index 000000000..8972602a5 --- /dev/null +++ b/addon-sdk/source/lib/sdk/lang/weak-set.js @@ -0,0 +1,75 @@ +/* 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/. */ + +module.metadata = { + "stability": "experimental" +}; + +"use strict"; + +const { Cu } = require("chrome"); + +function makeGetterFor(Type) { + let cache = new WeakMap(); + + return { + getFor(target) { + if (!cache.has(target)) + cache.set(target, new Type()); + + return cache.get(target); + }, + clearFor(target) { + return cache.delete(target) + } + } +} + +var {getFor: getLookupFor, clearFor: clearLookupFor} = makeGetterFor(WeakMap); +var {getFor: getRefsFor, clearFor: clearRefsFor} = makeGetterFor(Set); + +function add(target, value) { + if (has(target, value)) + return; + + getLookupFor(target).set(value, true); + getRefsFor(target).add(Cu.getWeakReference(value)); +} +exports.add = add; + +function remove(target, value) { + getLookupFor(target).delete(value); +} +exports.remove = remove; + +function has(target, value) { + return getLookupFor(target).has(value); +} +exports.has = has; + +function clear(target) { + clearLookupFor(target); + clearRefsFor(target); +} +exports.clear = clear; + +function iterator(target) { + let refs = getRefsFor(target); + + for (let ref of refs) { + let value = ref.get(); + + // If `value` is already gc'ed, it would be `null`. + // The `has` function is using a WeakMap as lookup table, so passing `null` + // would raise an exception because WeakMap accepts as value only non-null + // object. + // Plus, if `value` is already gc'ed, we do not have to take it in account + // during the iteration, and remove it from the references. + if (value !== null && has(target, value)) + yield value; + else + refs.delete(ref); + } +} +exports.iterator = iterator; diff --git a/addon-sdk/source/lib/sdk/loader/cuddlefish.js b/addon-sdk/source/lib/sdk/loader/cuddlefish.js new file mode 100644 index 000000000..6ba19157b --- /dev/null +++ b/addon-sdk/source/lib/sdk/loader/cuddlefish.js @@ -0,0 +1,102 @@ +/* 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" +}; + +// This module is manually loaded by bootstrap.js in a sandbox and immediatly +// put in module cache so that it is never loaded in any other way. + +/* Workarounds to include dependencies in the manifest +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 +*/ + +const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components; + +// `loadSandbox` is exposed by bootstrap.js +const loaderURI = module.uri.replace("sdk/loader/cuddlefish.js", + "toolkit/loader.js"); +const xulappURI = module.uri.replace("loader/cuddlefish.js", + "system/xul-app.jsm"); +// We need to keep a reference to the sandbox in order to unload it in +// bootstrap.js + +var loaderSandbox = loadSandbox(loaderURI); +const loaderModule = loaderSandbox.exports; + +const { incompatibility } = Cu.import(xulappURI, {}).XulApp; + +const { override, load } = loaderModule; + +function CuddlefishLoader(options) { + let { manifest } = options; + + options = override(options, { + // Put `api-utils/loader` and `api-utils/cuddlefish` loaded as JSM to module + // cache to avoid subsequent loads via `require`. + modules: override({ + 'toolkit/loader': loaderModule, + 'sdk/loader/cuddlefish': exports + }, options.modules), + resolve: function resolve(id, requirer) { + let entry = requirer && requirer in manifest && manifest[requirer]; + let uri = null; + + // If manifest entry for this requirement is present we follow manifest. + // Note: Standard library modules like 'panel' will be present in + // manifest unless they were moved to platform. + if (entry) { + let requirement = entry.requirements[id]; + // If requirer entry is in manifest and it's requirement is not, than + // it has no authority to load since linker was not able to find it. + if (!requirement) + throw Error('Module: ' + requirer + ' has no authority to load: ' + + id, requirer); + + uri = requirement; + } else { + // If requirer is off manifest than it's a system module and we allow it + // to go off manifest by resolving a relative path. + uri = loaderModule.resolve(id, requirer); + } + return uri; + }, + load: function(loader, module) { + let result; + let error; + + // In order to get the module's metadata, we need to load the module. + // if an exception is raised here, it could be that is due to application + // incompatibility. Therefore the exception is stored, and thrown again + // only if the module seems be compatible with the application currently + // running. Otherwise the incompatibility message takes the precedence. + try { + result = load(loader, module); + } + catch (e) { + error = e; + } + + error = incompatibility(module) || error; + + if (error) + throw error; + + return result; + } + }); + + let loader = loaderModule.Loader(options); + // Hack to allow loading from `toolkit/loader`. + loader.modules[loaderURI] = loaderSandbox; + return loader; +} + +exports = override(loaderModule, { + Loader: CuddlefishLoader +}); diff --git a/addon-sdk/source/lib/sdk/loader/sandbox.js b/addon-sdk/source/lib/sdk/loader/sandbox.js new file mode 100644 index 000000000..791dbc086 --- /dev/null +++ b/addon-sdk/source/lib/sdk/loader/sandbox.js @@ -0,0 +1,74 @@ +/* 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 { Cc, Ci, CC, Cu } = require('chrome'); +const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); +const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1']. + getService(Ci.mozIJSSubScriptLoader); +const self = require('sdk/self'); +const { getTabId } = require('../tabs/utils'); +const { getInnerId } = require('../window/utils'); + +const { devtools } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const { require: devtoolsRequire } = devtools; +const { addContentGlobal, removeContentGlobal } = devtoolsRequire("devtools/server/content-globals"); + +/** + * Make a new sandbox that inherits given `source`'s principals. Source can be + * URI string, DOMWindow or `null` for system principals. + */ +function sandbox(target, options) { + options = options || {}; + options.metadata = options.metadata ? options.metadata : {}; + options.metadata.addonID = options.metadata.addonID ? + options.metadata.addonID : self.id; + + let sandbox = Cu.Sandbox(target || systemPrincipal, options); + Cu.setSandboxMetadata(sandbox, options.metadata); + let innerWindowID = options.metadata['inner-window-id'] + if (innerWindowID) { + addContentGlobal({ + global: sandbox, + 'inner-window-id': innerWindowID + }); + } + return sandbox; +} +exports.sandbox = sandbox; + +/** + * Evaluates given `source` in a given `sandbox` and returns result. + */ +function evaluate(sandbox, code, uri, line, version) { + return Cu.evalInSandbox(code, sandbox, version || '1.8', uri || '', line || 1); +} +exports.evaluate = evaluate; + +/** + * Evaluates code under the given `uri` in the given `sandbox`. + * + * @param {String} uri + * The URL pointing to the script to load. + * It must be a local chrome:, resource:, file: or data: URL. + */ +function load(sandbox, uri) { + if (uri.indexOf('data:') === 0) { + let source = uri.substr(uri.indexOf(',') + 1); + + return evaluate(sandbox, decodeURIComponent(source), '1.8', uri, 0); + } else { + return scriptLoader.loadSubScript(uri, sandbox, 'UTF-8'); + } +} +exports.load = load; + +/** + * Forces the given `sandbox` to be freed immediately. + */ +exports.nuke = Cu.nukeSandbox diff --git a/addon-sdk/source/lib/sdk/messaging.js b/addon-sdk/source/lib/sdk/messaging.js new file mode 100644 index 000000000..07580eb33 --- /dev/null +++ b/addon-sdk/source/lib/sdk/messaging.js @@ -0,0 +1,12 @@ +/* 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 { window } = require("sdk/addon/window"); +exports.MessageChannel = window.MessageChannel; +exports.MessagePort = window.MessagePort; diff --git a/addon-sdk/source/lib/sdk/model/core.js b/addon-sdk/source/lib/sdk/model/core.js new file mode 100644 index 000000000..315f8b1cd --- /dev/null +++ b/addon-sdk/source/lib/sdk/model/core.js @@ -0,0 +1,23 @@ +/* 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 { dispatcher } = require("../util/dispatcher"); + + +// Define `modelFor` accessor function that can be implemented +// for different types of views. Since view's we'll be dealing +// with types that don't really play well with `instanceof` +// operator we're gonig to use `dispatcher` that is slight +// extension over polymorphic dispatch provided by method. +// This allows models to extend implementations of this by +// providing predicates: +// +// modelFor.when($ => $ && $.nodeName === "tab", findTabById($.id)) +const modelFor = dispatcher("modelFor"); +exports.modelFor = modelFor; diff --git a/addon-sdk/source/lib/sdk/net/url.js b/addon-sdk/source/lib/sdk/net/url.js new file mode 100644 index 000000000..5502171ee --- /dev/null +++ b/addon-sdk/source/lib/sdk/net/url.js @@ -0,0 +1,94 @@ +/* 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 { Ci, Cu, components } = require("chrome"); + +const { defer } = require("../core/promise"); +const { merge } = require("../util/object"); + +const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {}); +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +/** + * Reads a URI and returns a promise. + * + * @param uri {string} The URI to read + * @param [options] {object} This parameter can have any or all of the following + * fields: `charset`. By default the `charset` is set to 'UTF-8'. + * + * @returns {promise} The promise that will be resolved with the content of the + * URL given. + * + * @example + * let promise = readURI('resource://gre/modules/NetUtil.jsm', { + * charset: 'US-ASCII' + * }); + */ +function readURI(uri, options) { + options = options || {}; + let charset = options.charset || 'UTF-8'; + + let channel = NetUtil.newChannel({ + uri: NetUtil.newURI(uri, charset), + loadUsingSystemPrincipal: true}); + + let { promise, resolve, reject } = defer(); + + try { + NetUtil.asyncFetch(channel, function (stream, result) { + if (components.isSuccessCode(result)) { + let count = stream.available(); + let data = NetUtil.readInputStreamToString(stream, count, { charset : charset }); + + resolve(data); + } else { + reject("Failed to read: '" + uri + "' (Error Code: " + result + ")"); + } + }); + } + catch (e) { + reject("Failed to read: '" + uri + "' (Error: " + e.message + ")"); + } + + return promise; +} + +exports.readURI = readURI; + +/** + * Reads a URI synchronously. + * This function is intentionally undocumented to favorites the `readURI` usage. + * + * @param uri {string} The URI to read + * @param [charset] {string} The character set to use when read the content of + * the `uri` given. By default is set to 'UTF-8'. + * + * @returns {string} The content of the URI given. + * + * @example + * let data = readURISync('resource://gre/modules/NetUtil.jsm'); + */ +function readURISync(uri, charset) { + charset = typeof charset === "string" ? charset : "UTF-8"; + + let channel = NetUtil.newChannel({ + uri: NetUtil.newURI(uri, charset), + loadUsingSystemPrincipal: true}); + let stream = channel.open2(); + + let count = stream.available(); + let data = NetUtil.readInputStreamToString(stream, count, { charset : charset }); + + stream.close(); + + return data; +} + +exports.readURISync = readURISync; diff --git a/addon-sdk/source/lib/sdk/net/xhr.js b/addon-sdk/source/lib/sdk/net/xhr.js new file mode 100644 index 000000000..415b9cbf4 --- /dev/null +++ b/addon-sdk/source/lib/sdk/net/xhr.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": "stable" +}; + +const { deprecateFunction } = require("../util/deprecate"); +const { Cc, Ci } = require("chrome"); +const XMLHttpRequest = require("../addon/window").window.XMLHttpRequest; + +Object.defineProperties(XMLHttpRequest.prototype, { + mozBackgroundRequest: { + value: true, + }, + forceAllowThirdPartyCookie: { + configurable: true, + value: deprecateFunction(function() { + forceAllowThirdPartyCookie(this); + + }, "`xhr.forceAllowThirdPartyCookie()` is deprecated, please use" + + "`require('sdk/net/xhr').forceAllowThirdPartyCookie(request)` instead") + } +}); +exports.XMLHttpRequest = XMLHttpRequest; + +function forceAllowThirdPartyCookie(xhr) { + if (xhr.channel instanceof Ci.nsIHttpChannelInternal) + xhr.channel.forceAllowThirdPartyCookie = true; +} +exports.forceAllowThirdPartyCookie = forceAllowThirdPartyCookie; + +// No need to handle add-on unloads as addon/window is closed at unload +// and it will take down all the associated requests. diff --git a/addon-sdk/source/lib/sdk/notifications.js b/addon-sdk/source/lib/sdk/notifications.js new file mode 100644 index 000000000..752e08fb1 --- /dev/null +++ b/addon-sdk/source/lib/sdk/notifications.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/. */ + +"use strict"; + +module.metadata = { + "stability": "stable" +}; + +const { Cc, Ci, Cr } = require("chrome"); +const apiUtils = require("./deprecated/api-utils"); +const { isString, isUndefined, instanceOf } = require('./lang/type'); +const { URL, isLocalURL } = require('./url'); +const { data } = require('./self'); + +const NOTIFICATION_DIRECTIONS = ["auto", "ltr", "rtl"]; + +try { + let alertServ = Cc["@mozilla.org/alerts-service;1"]. + getService(Ci.nsIAlertsService); + + // The unit test sets this to a mock notification function. + var notify = alertServ.showAlertNotification.bind(alertServ); +} +catch (err) { + // An exception will be thrown if the platform doesn't provide an alert + // service, e.g., if Growl is not installed on OS X. In that case, use a + // mock notification function that just logs to the console. + notify = notifyUsingConsole; +} + +exports.notify = function notifications_notify(options) { + let valOpts = validateOptions(options); + let clickObserver = !valOpts.onClick ? null : { + observe: (subject, topic, data) => { + if (topic === "alertclickcallback") { + try { + valOpts.onClick.call(exports, valOpts.data); + } + catch(e) { + console.exception(e); + } + } + } + }; + function notifyWithOpts(notifyFn) { + let { iconURL } = valOpts; + iconURL = iconURL && isLocalURL(iconURL) ? data.url(iconURL) : iconURL; + + notifyFn(iconURL, valOpts.title, valOpts.text, !!clickObserver, + valOpts.data, clickObserver, valOpts.tag, valOpts.dir, valOpts.lang); + } + try { + notifyWithOpts(notify); + } + catch (err) { + if (err instanceof Ci.nsIException && err.result == Cr.NS_ERROR_FILE_NOT_FOUND) { + console.warn("The notification icon named by " + iconURL + + " does not exist. A default icon will be used instead."); + delete valOpts.iconURL; + notifyWithOpts(notify); + } + else { + notifyWithOpts(notifyUsingConsole); + } + } +}; + +function notifyUsingConsole(iconURL, title, text) { + title = title ? "[" + title + "]" : ""; + text = text || ""; + let str = [title, text].filter(s => s).join(" "); + console.log(str); +} + +function validateOptions(options) { + return apiUtils.validateOptions(options, { + data: { + is: ["string", "undefined"] + }, + iconURL: { + is: ["string", "undefined", "object"], + ok: function(value) { + return isUndefined(value) || isString(value) || (value instanceof URL); + }, + msg: "`iconURL` must be a string or an URL instance." + }, + onClick: { + is: ["function", "undefined"] + }, + text: { + is: ["string", "undefined", "number"] + }, + title: { + is: ["string", "undefined", "number"] + }, + tag: { + is: ["string", "undefined", "number"] + }, + dir: { + is: ["string", "undefined"], + ok: function(value) { + return isUndefined(value) || ~NOTIFICATION_DIRECTIONS.indexOf(value); + }, + msg: '`dir` option must be one of: "auto", "ltr" or "rtl".' + }, + lang: { + is: ["string", "undefined"] + } + }); +} diff --git a/addon-sdk/source/lib/sdk/output/system.js b/addon-sdk/source/lib/sdk/output/system.js new file mode 100644 index 000000000..4fb16dcd5 --- /dev/null +++ b/addon-sdk/source/lib/sdk/output/system.js @@ -0,0 +1,71 @@ +/* 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, Ci, Cr } = require("chrome"); +const { Input, start, stop, receive, outputs } = require("../event/utils"); +const { id: addonID } = require("../self"); +const { setImmediate } = require("../timers"); +const { notifyObservers } = Cc['@mozilla.org/observer-service;1']. + getService(Ci.nsIObserverService); + +const NOT_AN_INPUT = "OutputPort can be used only for sending messages"; + +// `OutputPort` creates a port to which messages can be send. Those +// messages are actually disptached as `subject`'s of the observer +// notifications. This is handy for communicating between different +// components of the SDK. By default messages are dispatched +// asynchronously, although `options.sync` can be used to make them +// synchronous. If `options.id` is given `topic` for observer +// notifications is generated by namespacing it, to avoid spamming +// other SDK add-ons. It's also possible to provide `options.topic` +// to use excat `topic` without namespacing it. +// +// Note: Symmetric `new InputPort({ id: "x" })` instances can be used to +// receive messages send to the instances of `new OutputPort({ id: "x" })`. +const OutputPort = function({id, topic, sync}) { + this.id = id || topic; + this.sync = !!sync; + this.topic = topic || "sdk:" + addonID + ":" + id; +}; +// OutputPort extends base signal type to implement same message +// receiving interface. +OutputPort.prototype = new Input(); +OutputPort.constructor = OutputPort; + +// OutputPort can not be consumed there for starting or stopping it +// is not supported. +OutputPort.prototype[start] = _ => { throw TypeError(NOT_AN_INPUT); }; +OutputPort.prototype[stop] = _ => { throw TypeError(NOT_AN_INPUT); }; + +// Port reecives message send to it, which will be dispatched via +// observer notification service. +OutputPort.receive = ({topic, sync}, message) => { + const type = typeof(message); + const supported = message === null || + type === "object" || + type === "function"; + + // There is no sensible way to wrap JS primitives that would make sense + // for general observer notification users. It's also probably not very + // useful to dispatch JS primitives as subject of observer service, there + // for we do not support those use cases. + if (!supported) + throw new TypeError("Unsupproted message type: `" + type + "`"); + + // Normalize `message` to create a valid observer notification `subject`. + // If `message` is `null`, implements `nsISupports` interface or already + // represents wrapped JS object use it as is. Otherwise create a wrapped + // object so that observers could receive it. + const subject = message === null ? null : + message instanceof Ci.nsISupports ? message : + message.wrappedJSObject ? message : + {wrappedJSObject: message}; + if (sync) + notifyObservers(subject, topic, null); + else + setImmediate(notifyObservers, subject, topic, null); +}; +OutputPort.prototype[receive] = OutputPort.receive; +exports.OutputPort = OutputPort; diff --git a/addon-sdk/source/lib/sdk/page-mod.js b/addon-sdk/source/lib/sdk/page-mod.js new file mode 100644 index 000000000..538be2732 --- /dev/null +++ b/addon-sdk/source/lib/sdk/page-mod.js @@ -0,0 +1,190 @@ +/* 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": "stable" +}; + +const { contract: loaderContract } = require('./content/loader'); +const { contract } = require('./util/contract'); +const { WorkerHost, connect } = require('./content/utils'); +const { Class } = require('./core/heritage'); +const { Disposable } = require('./core/disposable'); +const { Worker } = require('./content/worker'); +const { EventTarget } = require('./event/target'); +const { on, emit, once, setListeners } = require('./event/core'); +const { isRegExp, isUndefined } = require('./lang/type'); +const { merge, omit } = require('./util/object'); +const { remove, has, hasAny } = require("./util/array"); +const { Rules } = require("./util/rules"); +const { processes, frames, remoteRequire } = require('./remote/parent'); +remoteRequire('sdk/content/page-mod'); + +const pagemods = new Map(); +const workers = new Map(); +const models = new WeakMap(); +var modelFor = (mod) => models.get(mod); +var workerFor = (mod) => workers.get(mod)[0]; + +// Helper functions +var isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string'; + +var PAGEMOD_ID = 0; + +// Validation Contracts +const modOptions = { + // contentStyle* / contentScript* are sharing the same validation constraints, + // so they can be mostly reused, except for the messages. + contentStyle: merge(Object.create(loaderContract.rules.contentScript), { + msg: 'The `contentStyle` option must be a string or an array of strings.' + }), + contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), { + msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' + }), + include: { + is: ['string', 'array', 'regexp'], + ok: (rule) => { + if (isRegExpOrString(rule)) + return true; + if (Array.isArray(rule) && rule.length > 0) + return rule.every(isRegExpOrString); + return false; + }, + msg: 'The `include` option must always contain atleast one rule as a string, regular expression, or an array of strings and regular expressions.' + }, + exclude: { + is: ['string', 'array', 'regexp', 'undefined'], + ok: (rule) => { + if (isRegExpOrString(rule) || isUndefined(rule)) + return true; + if (Array.isArray(rule) && rule.length > 0) + return rule.every(isRegExpOrString); + return false; + }, + msg: 'If set, the `exclude` option must always contain at least one ' + + 'rule as a string, regular expression, or an array of strings and ' + + 'regular expressions.' + }, + attachTo: { + is: ['string', 'array', 'undefined'], + map: function (attachTo) { + if (!attachTo) return ['top', 'frame']; + if (typeof attachTo === 'string') return [attachTo]; + return attachTo; + }, + ok: function (attachTo) { + return hasAny(attachTo, ['top', 'frame']) && + attachTo.every(has.bind(null, ['top', 'frame', 'existing'])); + }, + msg: 'The `attachTo` option must be a string or an array of strings. ' + + 'The only valid options are "existing", "top" and "frame", and must ' + + 'contain at least "top" or "frame" values.' + }, +}; + +const modContract = contract(merge({}, loaderContract.rules, modOptions)); + +/** + * PageMod constructor (exported below). + * @constructor + */ +const PageMod = Class({ + implements: [ + modContract.properties(modelFor), + EventTarget, + Disposable, + ], + extends: WorkerHost(workerFor), + setup: function PageMod(options) { + let mod = this; + let model = modContract(options); + models.set(this, model); + model.id = PAGEMOD_ID++; + + let include = model.include; + model.include = Rules(); + model.include.add.apply(model.include, [].concat(include)); + + let exclude = isUndefined(model.exclude) ? [] : model.exclude; + model.exclude = Rules(); + model.exclude.add.apply(model.exclude, [].concat(exclude)); + + // Set listeners on {PageMod} itself, not the underlying worker, + // like `onMessage`, as it'll get piped. + setListeners(this, options); + + pagemods.set(model.id, this); + workers.set(this, []); + + function serializeRules(rules) { + for (let rule of rules) { + yield isRegExp(rule) ? { type: "regexp", pattern: rule.source, flags: rule.flags } + : { type: "string", value: rule }; + } + } + + model.childOptions = omit(model, ["include", "exclude", "contentScriptOptions"]); + model.childOptions.include = [...serializeRules(model.include)]; + model.childOptions.exclude = [...serializeRules(model.exclude)]; + model.childOptions.contentScriptOptions = model.contentScriptOptions ? + JSON.stringify(model.contentScriptOptions) : + null; + + processes.port.emit('sdk/page-mod/create', model.childOptions); + }, + + dispose: function(reason) { + processes.port.emit('sdk/page-mod/destroy', modelFor(this).id); + pagemods.delete(modelFor(this).id); + workers.delete(this); + }, + + destroy: function(reason) { + // Explicit destroy call, i.e. not via unload so destroy the workers + let list = workers.get(this); + if (!list) + return; + + // Triggers dispose which will cause the child page-mod to be destroyed + Disposable.prototype.destroy.call(this, reason); + + // Destroy any active workers + for (let worker of list) + worker.destroy(reason); + } +}); +exports.PageMod = PageMod; + +// Whenever a new process starts send over the list of page-mods +processes.forEvery(process => { + for (let mod of pagemods.values()) + process.port.emit('sdk/page-mod/create', modelFor(mod).childOptions); +}); + +frames.port.on('sdk/page-mod/worker-create', (frame, modId, workerOptions) => { + let mod = pagemods.get(modId); + if (!mod) + return; + + // Attach the parent side of the worker to the child + let worker = Worker(); + + workers.get(mod).unshift(worker); + worker.on('*', (event, ...args) => { + // page-mod's "attach" event needs to be passed a worker + if (event === 'attach') + emit(mod, event, worker) + else + emit(mod, event, ...args); + }); + + worker.on('detach', () => { + let array = workers.get(mod); + if (array) + remove(array, worker); + }); + + connect(worker, frame, workerOptions); +}); diff --git a/addon-sdk/source/lib/sdk/page-mod/match-pattern.js b/addon-sdk/source/lib/sdk/page-mod/match-pattern.js new file mode 100644 index 000000000..afbbd401e --- /dev/null +++ b/addon-sdk/source/lib/sdk/page-mod/match-pattern.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"; + +var { deprecateUsage } = require("../util/deprecate"); + +deprecateUsage("Module 'sdk/page-mod/match-pattern' is deprecated use 'sdk/util/match-pattern' instead"); + +module.exports = require("../util/match-pattern"); diff --git a/addon-sdk/source/lib/sdk/page-worker.js b/addon-sdk/source/lib/sdk/page-worker.js new file mode 100644 index 000000000..837cf774b --- /dev/null +++ b/addon-sdk/source/lib/sdk/page-worker.js @@ -0,0 +1,194 @@ +/* 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": "stable" +}; + +const { Class } = require('./core/heritage'); +const { ns } = require('./core/namespace'); +const { pipe, stripListeners } = require('./event/utils'); +const { connect, destroy, WorkerHost } = require('./content/utils'); +const { Worker } = require('./content/worker'); +const { Disposable } = require('./core/disposable'); +const { EventTarget } = require('./event/target'); +const { setListeners } = require('./event/core'); +const { window } = require('./addon/window'); +const { create: makeFrame, getDocShell } = require('./frame/utils'); +const { contract } = require('./util/contract'); +const { contract: loaderContract } = require('./content/loader'); +const { Rules } = require('./util/rules'); +const { merge } = require('./util/object'); +const { uuid } = require('./util/uuid'); +const { useRemoteProcesses, remoteRequire, frames } = require("./remote/parent"); +remoteRequire("sdk/content/page-worker"); + +const workers = new WeakMap(); +const pages = new Map(); + +const internal = ns(); + +let workerFor = (page) => workers.get(page); +let isDisposed = (page) => !pages.has(internal(page).id); + +// The frame is used to ensure we have a remote process to load workers in +let remoteFrame = null; +let framePromise = null; +function getFrame() { + if (framePromise) + return framePromise; + + framePromise = new Promise(resolve => { + let view = makeFrame(window.document, { + namespaceURI: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + nodeName: "iframe", + type: "content", + remote: useRemoteProcesses, + uri: "about:blank" + }); + + // Wait for the remote side to connect + let listener = (frame) => { + if (frame.frameElement != view) + return; + frames.off("attach", listener); + remoteFrame = frame; + resolve(frame); + } + frames.on("attach", listener); + }); + return framePromise; +} + +var pageContract = contract(merge({ + allow: { + is: ['object', 'undefined', 'null'], + map: function (allow) { return { script: !allow || allow.script !== false }} + }, + onMessage: { + is: ['function', 'undefined'] + }, + include: { + is: ['string', 'array', 'regexp', 'undefined'] + }, + contentScriptWhen: { + is: ['string', 'undefined'], + map: (when) => when || "end" + } +}, loaderContract.rules)); + +function enableScript (page) { + getDocShell(viewFor(page)).allowJavascript = true; +} + +function disableScript (page) { + getDocShell(viewFor(page)).allowJavascript = false; +} + +function Allow (page) { + return { + get script() { + return internal(page).options.allow.script; + }, + set script(value) { + internal(page).options.allow.script = value; + + if (isDisposed(page)) + return; + + remoteFrame.port.emit("sdk/frame/set", internal(page).id, { allowScript: value }); + } + }; +} + +function isValidURL(page, url) { + return !page.rules || page.rules.matchesAny(url); +} + +const Page = Class({ + implements: [ + EventTarget, + Disposable + ], + extends: WorkerHost(workerFor), + setup: function Page(options) { + options = pageContract(options); + // Sanitize the options + if ("contentScriptOptions" in options) + options.contentScriptOptions = JSON.stringify(options.contentScriptOptions); + + internal(this).id = uuid().toString(); + internal(this).options = options; + + for (let prop of ['contentScriptFile', 'contentScript', 'contentScriptWhen']) { + this[prop] = options[prop]; + } + + pages.set(internal(this).id, this); + + // Set listeners on the {Page} object itself, not the underlying worker, + // like `onMessage`, as it gets piped + setListeners(this, options); + let worker = new Worker(stripListeners(options)); + workers.set(this, worker); + pipe(worker, this); + + if (options.include) { + this.rules = Rules(); + this.rules.add.apply(this.rules, [].concat(options.include)); + } + + getFrame().then(frame => { + if (isDisposed(this)) + return; + + frame.port.emit("sdk/frame/create", internal(this).id, stripListeners(options)); + }); + }, + get allow() { return Allow(this); }, + set allow(value) { + if (isDisposed(this)) + return; + this.allow.script = pageContract({ allow: value }).allow.script; + }, + get contentURL() { + return internal(this).options.contentURL; + }, + set contentURL(value) { + if (!isValidURL(this, value)) + return; + internal(this).options.contentURL = value; + if (isDisposed(this)) + return; + + remoteFrame.port.emit("sdk/frame/set", internal(this).id, { contentURL: value }); + }, + dispose: function () { + if (isDisposed(this)) + return; + pages.delete(internal(this).id); + let worker = workerFor(this); + if (worker) + destroy(worker); + remoteFrame.port.emit("sdk/frame/destroy", internal(this).id); + + // Destroy the remote frame if all the pages have been destroyed + if (pages.size == 0) { + framePromise = null; + remoteFrame.frameElement.remove(); + remoteFrame = null; + } + }, + toString: function () { return '[object Page]' } +}); + +exports.Page = Page; + +frames.port.on("sdk/frame/connect", (frame, id, params) => { + let page = pages.get(id); + if (!page) + return; + connect(workerFor(page), frame, params); +}); diff --git a/addon-sdk/source/lib/sdk/panel.js b/addon-sdk/source/lib/sdk/panel.js new file mode 100644 index 000000000..4b625799d --- /dev/null +++ b/addon-sdk/source/lib/sdk/panel.js @@ -0,0 +1,427 @@ +/* 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 and SeaMonkey. +// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps +module.metadata = { + "stability": "stable", + "engines": { + "Firefox": "*", + "SeaMonkey": "*" + } +}; + +const { Cu, Ci } = require("chrome"); +const { setTimeout } = require('./timers'); +const { Class } = require("./core/heritage"); +const { merge } = require("./util/object"); +const { WorkerHost } = require("./content/utils"); +const { Worker } = require("./deprecated/sync-worker"); +const { Disposable } = require("./core/disposable"); +const { WeakReference } = require('./core/reference'); +const { contract: loaderContract } = require("./content/loader"); +const { contract } = require("./util/contract"); +const { on, off, emit, setListeners } = require("./event/core"); +const { EventTarget } = require("./event/target"); +const domPanel = require("./panel/utils"); +const { getDocShell } = require('./frame/utils'); +const { events } = require("./panel/events"); +const systemEvents = require("./system/events"); +const { filter, pipe, stripListeners } = require("./event/utils"); +const { getNodeView, getActiveView } = require("./view/core"); +const { isNil, isObject, isNumber } = require("./lang/type"); +const { getAttachEventType } = require("./content/utils"); +const { number, boolean, object } = require('./deprecated/api-utils'); +const { Style } = require("./stylesheet/style"); +const { attach, detach } = require("./content/mod"); + +var isRect = ({top, right, bottom, left}) => [top, right, bottom, left]. + some(value => isNumber(value) && !isNaN(value)); + +var isSDKObj = obj => obj instanceof Class; + +var rectContract = contract({ + top: number, + right: number, + bottom: number, + left: number +}); + +var position = { + is: object, + map: v => (isNil(v) || isSDKObj(v) || !isObject(v)) ? v : rectContract(v), + ok: v => isNil(v) || isSDKObj(v) || (isObject(v) && isRect(v)), + msg: 'The option "position" must be a SDK object registered as anchor; ' + + 'or an object with one or more of the following keys set to numeric ' + + 'values: top, right, bottom, left.' +} + +var displayContract = contract({ + width: number, + height: number, + focus: boolean, + position: position +}); + +var panelContract = contract(merge({ + // contentStyle* / contentScript* are sharing the same validation constraints, + // so they can be mostly reused, except for the messages. + contentStyle: merge(Object.create(loaderContract.rules.contentScript), { + msg: 'The `contentStyle` option must be a string or an array of strings.' + }), + contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), { + msg: 'The `contentStyleFile` option must be a local URL or an array of URLs' + }), + contextMenu: boolean, + allow: { + is: ['object', 'undefined', 'null'], + map: function (allow) { return { script: !allow || allow.script !== false }} + }, +}, displayContract.rules, loaderContract.rules)); + +function Allow(panel) { + return { + get script() { return getDocShell(viewFor(panel).backgroundFrame).allowJavascript; }, + set script(value) { return setScriptState(panel, value); }, + }; +} + +function setScriptState(panel, value) { + let view = viewFor(panel); + getDocShell(view.backgroundFrame).allowJavascript = value; + getDocShell(view.viewFrame).allowJavascript = value; + view.setAttribute("sdkscriptenabled", "" + value); +} + +function isDisposed(panel) { + return !views.has(panel); +} + +var panels = new WeakMap(); +var models = new WeakMap(); +var views = new WeakMap(); +var workers = new WeakMap(); +var styles = new WeakMap(); + +const viewFor = (panel) => views.get(panel); +const modelFor = (panel) => models.get(panel); +const panelFor = (view) => panels.get(view); +const workerFor = (panel) => workers.get(panel); +const styleFor = (panel) => styles.get(panel); + +function getPanelFromWeakRef(weakRef) { + if (!weakRef) { + return null; + } + let panel = weakRef.get(); + if (!panel) { + return null; + } + if (isDisposed(panel)) { + return null; + } + return panel; +} + +var SinglePanelManager = { + visiblePanel: null, + enqueuedPanel: null, + enqueuedPanelCallback: null, + // Calls |callback| with no arguments when the panel may be shown. + requestOpen: function(panelToOpen, callback) { + let currentPanel = getPanelFromWeakRef(SinglePanelManager.visiblePanel); + if (currentPanel || SinglePanelManager.enqueuedPanel) { + SinglePanelManager.enqueuedPanel = Cu.getWeakReference(panelToOpen); + SinglePanelManager.enqueuedPanelCallback = callback; + if (currentPanel && currentPanel.isShowing) { + currentPanel.hide(); + } + } else { + SinglePanelManager.notifyPanelCanOpen(panelToOpen, callback); + } + }, + notifyPanelCanOpen: function(panel, callback) { + let view = viewFor(panel); + // Can't pass an arrow function as the event handler because we need to be + // able to call |removeEventListener| later. + view.addEventListener("popuphidden", SinglePanelManager.onVisiblePanelHidden, true); + view.addEventListener("popupshown", SinglePanelManager.onVisiblePanelShown, false); + SinglePanelManager.enqueuedPanel = null; + SinglePanelManager.enqueuedPanelCallback = null; + SinglePanelManager.visiblePanel = Cu.getWeakReference(panel); + callback(); + }, + onVisiblePanelShown: function(event) { + let panel = panelFor(event.target); + if (SinglePanelManager.enqueuedPanel) { + // Another panel started waiting for |panel| to close before |panel| was + // even done opening. + panel.hide(); + } + }, + onVisiblePanelHidden: function(event) { + let view = event.target; + let panel = panelFor(view); + let currentPanel = getPanelFromWeakRef(SinglePanelManager.visiblePanel); + if (currentPanel && currentPanel != panel) { + return; + } + SinglePanelManager.visiblePanel = null; + view.removeEventListener("popuphidden", SinglePanelManager.onVisiblePanelHidden, true); + view.removeEventListener("popupshown", SinglePanelManager.onVisiblePanelShown, false); + let nextPanel = getPanelFromWeakRef(SinglePanelManager.enqueuedPanel); + let nextPanelCallback = SinglePanelManager.enqueuedPanelCallback; + if (nextPanel) { + SinglePanelManager.notifyPanelCanOpen(nextPanel, nextPanelCallback); + } + } +}; + +const Panel = Class({ + implements: [ + // Generate accessors for the validated properties that update model on + // set and return values from model on get. + panelContract.properties(modelFor), + EventTarget, + Disposable, + WeakReference + ], + extends: WorkerHost(workerFor), + setup: function setup(options) { + let model = merge({ + defaultWidth: 320, + defaultHeight: 240, + focus: true, + position: Object.freeze({}), + contextMenu: false + }, panelContract(options)); + model.ready = false; + models.set(this, model); + + if (model.contentStyle || model.contentStyleFile) { + styles.set(this, Style({ + uri: model.contentStyleFile, + source: model.contentStyle + })); + } + + // Setup view + let viewOptions = {allowJavascript: !model.allow || (model.allow.script !== false)}; + let view = domPanel.make(null, viewOptions); + panels.set(view, this); + views.set(this, view); + + // Load panel content. + domPanel.setURL(view, model.contentURL); + + // Allow context menu + domPanel.allowContextMenu(view, model.contextMenu); + + // Setup listeners. + setListeners(this, options); + let worker = new Worker(stripListeners(options)); + workers.set(this, worker); + + // pipe events from worker to a panel. + pipe(worker, this); + }, + dispose: function dispose() { + this.hide(); + off(this); + + workerFor(this).destroy(); + detach(styleFor(this)); + + domPanel.dispose(viewFor(this)); + + // Release circular reference between view and panel instance. This + // way view will be GC-ed. And panel as well once all the other refs + // will be removed from it. + views.delete(this); + }, + /* Public API: Panel.width */ + get width() { + return modelFor(this).width; + }, + set width(value) { + this.resize(value, this.height); + }, + /* Public API: Panel.height */ + get height() { + return modelFor(this).height; + }, + set height(value) { + this.resize(this.width, value); + }, + + /* Public API: Panel.focus */ + get focus() { + return modelFor(this).focus; + }, + + /* Public API: Panel.position */ + get position() { + return modelFor(this).position; + }, + + /* Public API: Panel.contextMenu */ + get contextMenu() { + return modelFor(this).contextMenu; + }, + set contextMenu(allow) { + let model = modelFor(this); + model.contextMenu = panelContract({ contextMenu: allow }).contextMenu; + domPanel.allowContextMenu(viewFor(this), model.contextMenu); + }, + + get contentURL() { + return modelFor(this).contentURL; + }, + set contentURL(value) { + let model = modelFor(this); + model.contentURL = panelContract({ contentURL: value }).contentURL; + domPanel.setURL(viewFor(this), model.contentURL); + // Detach worker so that messages send will be queued until it's + // reatached once panel content is ready. + workerFor(this).detach(); + }, + + get allow() { return Allow(this); }, + set allow(value) { + let allowJavascript = panelContract({ allow: value }).allow.script; + return setScriptState(this, value); + }, + + /* Public API: Panel.isShowing */ + get isShowing() { + return !isDisposed(this) && domPanel.isOpen(viewFor(this)); + }, + + /* Public API: Panel.show */ + show: function show(options={}, anchor) { + SinglePanelManager.requestOpen(this, () => { + if (options instanceof Ci.nsIDOMElement) { + [anchor, options] = [options, null]; + } + + if (anchor instanceof Ci.nsIDOMElement) { + console.warn( + "Passing a DOM node to Panel.show() method is an unsupported " + + "feature that will be soon replaced. " + + "See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877" + ); + } + + let model = modelFor(this); + let view = viewFor(this); + let anchorView = getNodeView(anchor || options.position || model.position); + + options = merge({ + position: model.position, + width: model.width, + height: model.height, + defaultWidth: model.defaultWidth, + defaultHeight: model.defaultHeight, + focus: model.focus, + contextMenu: model.contextMenu + }, displayContract(options)); + + if (!isDisposed(this)) { + domPanel.show(view, options, anchorView); + } + }); + return this; + }, + + /* Public API: Panel.hide */ + hide: function hide() { + // Quit immediately if panel is disposed or there is no state change. + domPanel.close(viewFor(this)); + + return this; + }, + + /* Public API: Panel.resize */ + resize: function resize(width, height) { + let model = modelFor(this); + let view = viewFor(this); + let change = panelContract({ + width: width || model.width || model.defaultWidth, + height: height || model.height || model.defaultHeight + }); + + model.width = change.width + model.height = change.height + + domPanel.resize(view, model.width, model.height); + + return this; + } +}); +exports.Panel = Panel; + +// Note must be defined only after value to `Panel` is assigned. +getActiveView.define(Panel, viewFor); + +// Filter panel events to only panels that are create by this module. +var panelEvents = filter(events, ({target}) => panelFor(target)); + +// Panel events emitted after panel has being shown. +var shows = filter(panelEvents, ({type}) => type === "popupshown"); + +// Panel events emitted after panel became hidden. +var hides = filter(panelEvents, ({type}) => type === "popuphidden"); + +// Panel events emitted after content inside panel is ready. For different +// panels ready may mean different state based on `contentScriptWhen` attribute. +// Weather given event represents readyness is detected by `getAttachEventType` +// helper function. +var ready = filter(panelEvents, ({type, target}) => + getAttachEventType(modelFor(panelFor(target))) === type); + +// Panel event emitted when the contents of the panel has been loaded. +var readyToShow = filter(panelEvents, ({type}) => type === "DOMContentLoaded"); + +// Styles should be always added as soon as possible, and doesn't makes them +// depends on `contentScriptWhen` +var start = filter(panelEvents, ({type}) => type === "document-element-inserted"); + +// Forward panel show / hide events to panel's own event listeners. +on(shows, "data", ({target}) => { + let panel = panelFor(target); + if (modelFor(panel).ready) + emit(panel, "show"); +}); + +on(hides, "data", ({target}) => { + let panel = panelFor(target); + if (modelFor(panel).ready) + emit(panel, "hide"); +}); + +on(ready, "data", ({target}) => { + let panel = panelFor(target); + let window = domPanel.getContentDocument(target).defaultView; + + workerFor(panel).attach(window); +}); + +on(readyToShow, "data", ({target}) => { + let panel = panelFor(target); + + if (!modelFor(panel).ready) { + modelFor(panel).ready = true; + + if (viewFor(panel).state == "open") + emit(panel, "show"); + } +}); + +on(start, "data", ({target}) => { + let panel = panelFor(target); + let window = domPanel.getContentDocument(target).defaultView; + + attach(styleFor(panel), window); +}); diff --git a/addon-sdk/source/lib/sdk/panel/events.js b/addon-sdk/source/lib/sdk/panel/events.js new file mode 100644 index 000000000..f3040a11d --- /dev/null +++ b/addon-sdk/source/lib/sdk/panel/events.js @@ -0,0 +1,27 @@ +/* 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"; + +// This module basically translates system/events to a SDK standard events +// so that `map`, `filter` and other utilities could be used with them. + +module.metadata = { + "stability": "experimental" +}; + +const events = require("../system/events"); +const { emit } = require("../event/core"); + +var channel = {}; + +function forward({ subject, type, data }) { + return emit(channel, "data", { target: subject, type: type, data: data }); +} + +["popupshowing", "popuphiding", "popupshown", "popuphidden", +"document-element-inserted", "DOMContentLoaded", "load" +].forEach(type => events.on(type, forward)); + +exports.events = channel; diff --git a/addon-sdk/source/lib/sdk/panel/utils.js b/addon-sdk/source/lib/sdk/panel/utils.js new file mode 100644 index 000000000..c85b274bc --- /dev/null +++ b/addon-sdk/source/lib/sdk/panel/utils.js @@ -0,0 +1,451 @@ +/* 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 { Services } = require("resource://gre/modules/Services.jsm"); +const { setTimeout } = require("../timers"); +const { platform } = require("../system"); +const { getMostRecentBrowserWindow, getOwnerBrowserWindow, + getHiddenWindow, getScreenPixelsPerCSSPixel } = require("../window/utils"); + +const { create: createFrame, swapFrameLoaders, getDocShell } = require("../frame/utils"); +const { window: addonWindow } = require("../addon/window"); +const { isNil } = require("../lang/type"); +const { data } = require('../self'); + +const events = require("../system/events"); + + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +function calculateRegion({ position, width, height, defaultWidth, defaultHeight }, rect) { + position = position || {}; + + let x, y; + + let hasTop = !isNil(position.top); + let hasRight = !isNil(position.right); + let hasBottom = !isNil(position.bottom); + let hasLeft = !isNil(position.left); + let hasWidth = !isNil(width); + let hasHeight = !isNil(height); + + // if width is not specified by constructor or show's options, then get + // the default width + if (!hasWidth) + width = defaultWidth; + + // if height is not specified by constructor or show's options, then get + // the default height + if (!hasHeight) + height = defaultHeight; + + // default position is centered + x = (rect.right - width) / 2; + y = (rect.top + rect.bottom - height) / 2; + + if (hasTop) { + y = rect.top + position.top; + + if (hasBottom && !hasHeight) + height = rect.bottom - position.bottom - y; + } + else if (hasBottom) { + y = rect.bottom - position.bottom - height; + } + + if (hasLeft) { + x = position.left; + + if (hasRight && !hasWidth) + width = rect.right - position.right - x; + } + else if (hasRight) { + x = rect.right - width - position.right; + } + + return {x: x, y: y, width: width, height: height}; +} + +function open(panel, options, anchor) { + // Wait for the XBL binding to be constructed + if (!panel.openPopup) setTimeout(open, 50, panel, options, anchor); + else display(panel, options, anchor); +} +exports.open = open; + +function isOpen(panel) { + return panel.state === "open" +} +exports.isOpen = isOpen; + +function isOpening(panel) { + return panel.state === "showing" +} +exports.isOpening = isOpening + +function close(panel) { + // Sometimes "TypeError: panel.hidePopup is not a function" is thrown + // when quitting the host application while a panel is visible. To suppress + // these errors, check for "hidePopup" in panel before calling it. + // It's not clear if there's an issue or it's expected behavior. + // See Bug 1151796. + + return panel.hidePopup && panel.hidePopup(); +} +exports.close = close + + +function resize(panel, width, height) { + // Resize the iframe instead of using panel.sizeTo + // because sizeTo doesn't work with arrow panels + if (panel.firstChild) { + panel.firstChild.style.width = width + "px"; + panel.firstChild.style.height = height + "px"; + } +} +exports.resize = resize + +function display(panel, options, anchor) { + let document = panel.ownerDocument; + + let x, y; + let { width, height, defaultWidth, defaultHeight } = options; + + let popupPosition = null; + + // Panel XBL has some SDK incompatible styling decisions. We shim panel + // instances until proper fix for Bug 859504 is shipped. + shimDefaultStyle(panel); + + if (!anchor) { + // The XUL Panel doesn't have an arrow, so the margin needs to be reset + // in order to, be positioned properly + panel.style.margin = "0"; + + let viewportRect = document.defaultView.gBrowser.getBoundingClientRect(); + + ({x, y, width, height} = calculateRegion(options, viewportRect)); + } + else { + // The XUL Panel has an arrow, so the margin needs to be reset + // to the default value. + panel.style.margin = ""; + let { CustomizableUI, window } = anchor.ownerDocument.defaultView; + + // In Australis, widgets may be positioned in an overflow panel or the + // menu panel. + // In such cases clicking this widget will hide the overflow/menu panel, + // and the widget's panel will show instead. + // If `CustomizableUI` is not available, it means the anchor is not in a + // chrome browser window, and therefore there is no need for this check. + if (CustomizableUI) { + let node = anchor; + ({anchor} = CustomizableUI.getWidget(anchor.id).forWindow(window)); + + // if `node` is not the `anchor` itself, it means the widget is + // positioned in a panel, therefore we have to hide it before show + // the widget's panel in the same anchor + if (node !== anchor) + CustomizableUI.hidePanelForNode(anchor); + } + + width = width || defaultWidth; + height = height || defaultHeight; + + // Open the popup by the anchor. + let rect = anchor.getBoundingClientRect(); + + let zoom = getScreenPixelsPerCSSPixel(window); + let screenX = rect.left + window.mozInnerScreenX * zoom; + let screenY = rect.top + window.mozInnerScreenY * zoom; + + // Set up the vertical position of the popup relative to the anchor + // (always display the arrow on anchor center) + let horizontal, vertical; + if (screenY > window.screen.availHeight / 2 + height) + vertical = "top"; + else + vertical = "bottom"; + + if (screenY > window.screen.availWidth / 2 + width) + horizontal = "left"; + else + horizontal = "right"; + + let verticalInverse = vertical == "top" ? "bottom" : "top"; + popupPosition = vertical + "center " + verticalInverse + horizontal; + + // Allow panel to flip itself if the panel can't be displayed at the + // specified position (useful if we compute a bad position or if the + // user moves the window and panel remains visible) + panel.setAttribute("flip", "both"); + } + + if (!panel.viewFrame) { + panel.viewFrame = document.importNode(panel.backgroundFrame, false); + panel.appendChild(panel.viewFrame); + + let {privateBrowsingId} = getDocShell(panel.viewFrame).getOriginAttributes(); + let principal = Services.scriptSecurityManager.createNullPrincipal({privateBrowsingId}); + getDocShell(panel.viewFrame).createAboutBlankContentViewer(principal); + } + + // Resize the iframe instead of using panel.sizeTo + // because sizeTo doesn't work with arrow panels + panel.firstChild.style.width = width + "px"; + panel.firstChild.style.height = height + "px"; + + panel.openPopup(anchor, popupPosition, x, y); +} +exports.display = display; + +// This utility function is just a workaround until Bug 859504 has shipped. +function shimDefaultStyle(panel) { + let document = panel.ownerDocument; + // Please note that `panel` needs to be part of document in order to reach + // it's anonymous nodes. One of the anonymous node has a big padding which + // doesn't work well since panel frame needs to fill all of the panel. + // XBL binding is a not the best option as it's applied asynchronously, and + // makes injected frames behave in strange way. Also this feels a lot + // cheaper to do. + ["panel-inner-arrowcontent", "panel-arrowcontent"].forEach(function(value) { + let node = document.getAnonymousElementByAttribute(panel, "class", value); + if (node) node.style.padding = 0; + }); +} + +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); + + open(panel, options, anchor); +} +exports.show = show + +function onPanelClick(event) { + let { target, metaKey, ctrlKey, shiftKey, button } = event; + let accel = platform === "darwin" ? metaKey : ctrlKey; + let isLeftClick = button === 0; + let isMiddleClick = button === 1; + + if ((isLeftClick && (accel || shiftKey)) || isMiddleClick) { + let link = target.closest('a'); + + if (link && link.href) + getMostRecentBrowserWindow().openUILink(link.href, event) + } +} + +function setupPanelFrame(frame) { + frame.setAttribute("flex", 1); + frame.setAttribute("transparent", "transparent"); + frame.setAttribute("autocompleteenabled", true); + frame.setAttribute("tooltip", "aHTMLTooltip"); + if (platform === "darwin") { + frame.style.borderRadius = "var(--arrowpanel-border-radius, 3.5px)"; + frame.style.padding = "1px"; + } +} + +function make(document, options) { + document = document || getMostRecentBrowserWindow().document; + let panel = document.createElementNS(XUL_NS, "panel"); + panel.setAttribute("type", "arrow"); + panel.setAttribute("sdkscriptenabled", options.allowJavascript); + + // The panel needs to be attached to a browser window in order for us + // to copy browser styles to the content document when it loads. + attach(panel, document); + + let frameOptions = { + allowJavascript: options.allowJavascript, + allowPlugins: true, + allowAuth: true, + allowWindowControl: false, + // Need to override `nodeName` to use `iframe` as `browsers` save session + // history and in consequence do not dispatch "inner-window-destroyed" + // notifications. + browser: false, + }; + + let backgroundFrame = createFrame(addonWindow, frameOptions); + setupPanelFrame(backgroundFrame); + + getDocShell(backgroundFrame).inheritPrivateBrowsingId = false; + + function onPopupShowing({type, target}) { + if (target === this) { + let attrs = getDocShell(backgroundFrame).getOriginAttributes(); + getDocShell(panel.viewFrame).setOriginAttributes(attrs); + + swapFrameLoaders(backgroundFrame, panel.viewFrame); + } + } + + function onPopupHiding({type, target}) { + if (target === this) { + swapFrameLoaders(backgroundFrame, panel.viewFrame); + + panel.viewFrame.remove(); + panel.viewFrame = null; + } + } + + function onContentReady({target, type}) { + if (target === getContentDocument(panel)) { + style(panel); + events.emit(type, { subject: panel }); + } + } + + function onContentLoad({target, type}) { + if (target === getContentDocument(panel)) + events.emit(type, { subject: panel }); + } + + function onContentChange({subject: document, type}) { + if (document === getContentDocument(panel) && document.defaultView) + events.emit(type, { subject: panel }); + } + + function onPanelStateChange({target, type}) { + if (target === this) + events.emit(type, { subject: panel }) + } + + panel.addEventListener("popupshowing", onPopupShowing); + panel.addEventListener("popuphiding", onPopupHiding); + for (let event of ["popupshowing", "popuphiding", "popupshown", "popuphidden"]) + panel.addEventListener(event, onPanelStateChange); + + panel.addEventListener("click", onPanelClick, false); + + // Panel content document can be either in panel `viewFrame` or in + // a `backgroundFrame` depending on panel state. Listeners are set + // on both to avoid setting and removing listeners on panel state changes. + + panel.addEventListener("DOMContentLoaded", onContentReady, true); + backgroundFrame.addEventListener("DOMContentLoaded", onContentReady, true); + + panel.addEventListener("load", onContentLoad, true); + backgroundFrame.addEventListener("load", onContentLoad, true); + + events.on("document-element-inserted", onContentChange); + + panel.backgroundFrame = backgroundFrame; + panel.viewFrame = null; + + // Store event listener on the panel instance so that it won't be GC-ed + // while panel is alive. + panel.onContentChange = onContentChange; + + return panel; +} +exports.make = make; + +function attach(panel, document) { + document = document || getMostRecentBrowserWindow().document; + let container = document.getElementById("mainPopupSet"); + if (container !== panel.parentNode) { + detach(panel); + document.getElementById("mainPopupSet").appendChild(panel); + } +} +exports.attach = attach; + +function detach(panel) { + if (panel.parentNode) panel.parentNode.removeChild(panel); +} +exports.detach = detach; + +function dispose(panel) { + panel.backgroundFrame.remove(); + panel.backgroundFrame = null; + events.off("document-element-inserted", panel.onContentChange); + panel.onContentChange = null; + detach(panel); +} +exports.dispose = dispose; + +function style(panel) { + /** + Injects default OS specific panel styles into content document that is loaded + into given panel. Optionally `document` of the browser window can be + given to inherit styles from it, by default it will use either panel owner + document or an active browser's document. It should not matter though unless + Firefox decides to style windows differently base on profile or mode like + chrome for example. + **/ + + try { + let document = panel.ownerDocument; + let contentDocument = getContentDocument(panel); + let window = document.defaultView; + let node = document.getAnonymousElementByAttribute(panel, "class", + "panel-arrowcontent"); + + let { color, fontFamily, fontSize, fontWeight } = window.getComputedStyle(node); + + let style = contentDocument.createElement("style"); + style.id = "sdk-panel-style"; + style.textContent = "body { " + + "color: " + color + ";" + + "font-family: " + fontFamily + ";" + + "font-weight: " + fontWeight + ";" + + "font-size: " + fontSize + ";" + + "}"; + + let container = contentDocument.head ? contentDocument.head : + contentDocument.documentElement; + + if (container.firstChild) + container.insertBefore(style, container.firstChild); + else + container.appendChild(style); + } + catch (error) { + console.error("Unable to apply panel style"); + console.exception(error); + } +} +exports.style = style; + +var getContentFrame = panel => panel.viewFrame || panel.backgroundFrame; +exports.getContentFrame = getContentFrame; + +function getContentDocument(panel) { + return getContentFrame(panel).contentDocument; +} +exports.getContentDocument = getContentDocument; + +function setURL(panel, url) { + let frame = getContentFrame(panel); + let webNav = getDocShell(frame).QueryInterface(Ci.nsIWebNavigation); + + webNav.loadURI(url ? data.url(url) : "about:blank", 0, null, null, null); +} + +exports.setURL = setURL; + +function allowContextMenu(panel, allow) { + if (allow) { + panel.setAttribute("context", "contentAreaContextMenu"); + } + else { + panel.removeAttribute("context"); + } +} +exports.allowContextMenu = allowContextMenu; diff --git a/addon-sdk/source/lib/sdk/passwords.js b/addon-sdk/source/lib/sdk/passwords.js new file mode 100644 index 000000000..70f0aa4da --- /dev/null +++ b/addon-sdk/source/lib/sdk/passwords.js @@ -0,0 +1,61 @@ +/* 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": "stable" +}; + +const { search, remove, store } = require("./passwords/utils"); +const { defer, delay } = require("./lang/functional"); + +/** + * Utility function that returns `onComplete` and `onError` callbacks form the + * given `options` objects. Also properties are removed from the passed + * `options` objects. + * @param {Object} options + * Object that is passed to the exported functions of this module. + * @returns {Function[]} + * Array with two elements `onComplete` and `onError` functions. + */ +function getCallbacks(options) { + let value = [ + 'onComplete' in options ? options.onComplete : null, + 'onError' in options ? defer(options.onError) : console.exception + ]; + + delete options.onComplete; + delete options.onError; + + return value; +}; + +/** + * Creates a wrapper function that tries to call `onComplete` with a return + * value of the wrapped function or falls back to `onError` if wrapped function + * throws an exception. + */ +function createWrapperMethod(wrapped) { + return function (options) { + let [ onComplete, onError ] = getCallbacks(options); + try { + let value = wrapped(options); + if (onComplete) { + delay(function() { + try { + onComplete(value); + } catch (exception) { + onError(exception); + } + }); + } + } catch (exception) { + onError(exception); + } + }; +} + +exports.search = createWrapperMethod(search); +exports.store = createWrapperMethod(store); +exports.remove = createWrapperMethod(remove); diff --git a/addon-sdk/source/lib/sdk/passwords/utils.js b/addon-sdk/source/lib/sdk/passwords/utils.js new file mode 100644 index 000000000..334efa490 --- /dev/null +++ b/addon-sdk/source/lib/sdk/passwords/utils.js @@ -0,0 +1,107 @@ +/* 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, CC } = require("chrome"); +const { uri: ADDON_URI } = require("../self"); +const loginManager = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); +const { URL: parseURL } = require("../url"); +const LoginInfo = CC("@mozilla.org/login-manager/loginInfo;1", + "nsILoginInfo", "init"); + +function filterMatchingLogins(loginInfo) { + return Object.keys(this).every(key => loginInfo[key] === this[key], this); +} + +/** + * Removes `user`, `password` and `path` fields from the given `url` if it's + * 'http', 'https' or 'ftp'. All other URLs are returned unchanged. + * @example + * http://user:pass@www.site.com/foo/?bar=baz#bang -> http://www.site.com + */ +function normalizeURL(url) { + let { scheme, host, port } = parseURL(url); + // We normalize URL only if it's `http`, `https` or `ftp`. All other types of + // URLs (`resource`, `chrome`, etc..) should not be normalized as they are + // used with add-on associated credentials path. + return scheme === "http" || scheme === "https" || scheme === "ftp" ? + scheme + "://" + (host || "") + (port ? ":" + port : "") : + url +} + +function Login(options) { + let login = Object.create(Login.prototype); + Object.keys(options || {}).forEach(function(key) { + if (key === 'url') + login.hostname = normalizeURL(options.url); + else if (key === 'formSubmitURL') + login.formSubmitURL = options.formSubmitURL ? + normalizeURL(options.formSubmitURL) : null; + else if (key === 'realm') + login.httpRealm = options.realm; + else + login[key] = options[key]; + }); + + return login; +} +Login.prototype.toJSON = function toJSON() { + return { + url: this.hostname || ADDON_URI, + realm: this.httpRealm || null, + formSubmitURL: this.formSubmitURL || null, + username: this.username || null, + password: this.password || null, + usernameField: this.usernameField || '', + passwordField: this.passwordField || '', + } +}; +Login.prototype.toLoginInfo = function toLoginInfo() { + let { url, realm, formSubmitURL, username, password, usernameField, + passwordField } = this.toJSON(); + + return new LoginInfo(url, formSubmitURL, realm, username, password, + usernameField, passwordField); +}; + +function loginToJSON(value) { + return Login(value).toJSON(); +} + +/** + * Returns array of `nsILoginInfo` objects that are stored in the login manager + * and have all the properties with matching values as a given `options` object. + * @param {Object} options + * @returns {nsILoginInfo[]} + */ +exports.search = function search(options) { + return loginManager.getAllLogins() + .filter(filterMatchingLogins, Login(options)) + .map(loginToJSON); +}; + +/** + * Stores login info created from the given `options` to the applications + * built-in login management system. + * @param {Object} options. + */ +exports.store = function store(options) { + loginManager.addLogin(Login(options).toLoginInfo()); +}; + +/** + * Removes login info from the applications built-in login management system. + * _Please note: When removing a login info the specified properties must + * exactly match to the one that is already stored or exception will be thrown._ + * @param {Object} options. + */ +exports.remove = function remove(options) { + loginManager.removeLogin(Login(options).toLoginInfo()); +}; diff --git a/addon-sdk/source/lib/sdk/places/bookmarks.js b/addon-sdk/source/lib/sdk/places/bookmarks.js new file mode 100644 index 000000000..c4f9528f1 --- /dev/null +++ b/addon-sdk/source/lib/sdk/places/bookmarks.js @@ -0,0 +1,395 @@ +/* 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", + "engines": { + "Firefox": "*", + "SeaMonkey": "*" + } +}; + +/* + * Requiring hosts so they can subscribe to client messages + */ +require('./host/host-bookmarks'); +require('./host/host-tags'); +require('./host/host-query'); + +const { Cc, Ci } = require('chrome'); +const { Class } = require('../core/heritage'); +const { send } = require('../addon/events'); +const { defer, reject, all, resolve, promised } = require('../core/promise'); +const { EventTarget } = require('../event/target'); +const { emit } = require('../event/core'); +const { identity, defer:async } = require('../lang/functional'); +const { extend, merge } = require('../util/object'); +const { fromIterator } = require('../util/array'); +const { + constructTree, fetchItem, createQuery, + isRootGroup, createQueryOptions +} = require('./utils'); +const { + bookmarkContract, groupContract, separatorContract +} = require('./contract'); +const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +/* + * Mapping of uncreated bookmarks with their created + * counterparts + */ +const itemMap = new WeakMap(); + +/* + * Constant used by nsIHistoryQuery; 1 is a bookmark query + * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions + */ +const BOOKMARK_QUERY = 1; + +/* + * Bookmark Item classes + */ + +const Bookmark = Class({ + extends: [ + bookmarkContract.properties(identity) + ], + initialize: function initialize (options) { + merge(this, bookmarkContract(extend(defaults, options))); + }, + type: 'bookmark', + toString: () => '[object Bookmark]' +}); +exports.Bookmark = Bookmark; + +const Group = Class({ + extends: [ + groupContract.properties(identity) + ], + initialize: function initialize (options) { + // Don't validate if root group + if (isRootGroup(options)) + merge(this, options); + else + merge(this, groupContract(extend(defaults, options))); + }, + type: 'group', + toString: () => '[object Group]' +}); +exports.Group = Group; + +const Separator = Class({ + extends: [ + separatorContract.properties(identity) + ], + initialize: function initialize (options) { + merge(this, separatorContract(extend(defaults, options))); + }, + type: 'separator', + toString: () => '[object Separator]' +}); +exports.Separator = Separator; + +/* + * Functions + */ + +function save (items, options) { + items = [].concat(items); + options = options || {}; + let emitter = EventTarget(); + let results = []; + let errors = []; + let root = constructTree(items); + let cache = new Map(); + + let isExplicitSave = item => !!~items.indexOf(item); + // `walk` returns an aggregate promise indicating the completion + // of the `commitItem` on each node, not whether or not that + // commit was successful + + // Force this to be async, as if a ducktype fails validation, + // the promise implementation will fire an error event, which will + // not trigger the handler as it's not yet bound + // + // Can remove after `Promise.jsm` is implemented in Bug 881047, + // which will guarantee next tick execution + async(() => root.walk(preCommitItem).then(commitComplete))(); + + function preCommitItem ({value:item}) { + // Do nothing if tree root, default group (unsavable), + // or if it's a dependency and not explicitly saved (in the list + // of items to be saved), and not needed to be saved + if (item === null || // node is the tree root + isRootGroup(item) || + (getId(item) && !isExplicitSave(item))) + return; + + return promised(validate)(item) + .then(() => commitItem(item, options)) + .then(data => construct(data, cache)) + .then(savedItem => { + // If item was just created, make a map between + // the creation object and created object, + // so we can reference the item that doesn't have an id + if (!getId(item)) + saveId(item, savedItem.id); + + // Emit both the processed item, and original item + // so a mapping can be understood in handler + emit(emitter, 'data', savedItem, item); + + // Push to results iff item was explicitly saved + if (isExplicitSave(item)) + results[items.indexOf(item)] = savedItem; + }, reason => { + // Force reason to be a string for consistency + reason = reason + ''; + // Emit both the reason, and original item + // so a mapping can be understood in handler + emit(emitter, 'error', reason + '', item); + // Store unsaved item in results list + results[items.indexOf(item)] = item; + errors.push(reason); + }); + } + + // Called when traversal of the node tree is completed and all + // items have been committed + function commitComplete () { + emit(emitter, 'end', results); + } + + return emitter; +} +exports.save = save; + +function search (queries, options) { + queries = [].concat(queries); + let emitter = EventTarget(); + let cache = new Map(); + let queryObjs = queries.map(createQuery.bind(null, BOOKMARK_QUERY)); + let optionsObj = createQueryOptions(BOOKMARK_QUERY, options); + + // Can remove after `Promise.jsm` is implemented in Bug 881047, + // which will guarantee next tick execution + async(() => { + send('sdk-places-query', { queries: queryObjs, options: optionsObj }) + .then(handleQueryResponse); + })(); + + function handleQueryResponse (data) { + let deferreds = data.map(item => { + return construct(item, cache).then(bookmark => { + emit(emitter, 'data', bookmark); + return bookmark; + }, reason => { + emit(emitter, 'error', reason); + errors.push(reason); + }); + }); + + all(deferreds).then(data => { + emit(emitter, 'end', data); + }, () => emit(emitter, 'end', [])); + } + + return emitter; +} +exports.search = search; + +function remove (items) { + return [].concat(items).map(item => { + item.remove = true; + return item; + }); +} + +exports.remove = remove; + +/* + * Internal Utilities + */ + +function commitItem (item, options) { + // Get the item's ID, or getId it's saved version if it exists + let id = getId(item); + let data = normalize(item); + let promise; + + data.id = id; + + if (!id) { + promise = send('sdk-places-bookmarks-create', data); + } else if (item.remove) { + promise = send('sdk-places-bookmarks-remove', { id: id }); + } else { + promise = send('sdk-places-bookmarks-last-updated', { + id: id + }).then(function (updated) { + // If attempting to save an item that is not the + // latest snapshot of a bookmark item, execute + // the resolution function + if (updated !== item.updated && options.resolve) + return fetchItem(id) + .then(options.resolve.bind(null, data)); + else + return data; + }).then(send.bind(null, 'sdk-places-bookmarks-save')); + } + + return promise; +} + +/* + * Turns a bookmark item into a plain object, + * converts `tags` from Set to Array, group instance to an id + */ +function normalize (item) { + let data = merge({}, item); + // Circumvent prototype property of `type` + delete data.type; + data.type = item.type; + data.tags = []; + if (item.tags) { + data.tags = fromIterator(item.tags); + } + data.group = getId(data.group) || exports.UNSORTED.id; + + return data; +} + +/* + * Takes a data object and constructs a BookmarkItem instance + * of it, recursively generating parent instances as well. + * + * Pass in a `cache` Map to reuse instances of + * bookmark items to reduce overhead; + * The cache object is a map of id to a deferred with a + * promise that resolves to the bookmark item. + */ +function construct (object, cache, forced) { + let item = instantiate(object); + let deferred = defer(); + + // Item could not be instantiated + if (!item) + return resolve(null); + + // Return promise for item if found in the cache, + // and not `forced`. `forced` indicates that this is the construct + // call that should not read from cache, but should actually perform + // the construction, as it was set before several async calls + if (cache.has(item.id) && !forced) + return cache.get(item.id).promise; + else if (cache.has(item.id)) + deferred = cache.get(item.id); + else + cache.set(item.id, deferred); + + // When parent group is found in cache, use + // the same deferred value + if (item.group && cache.has(item.group)) { + cache.get(item.group).promise.then(group => { + item.group = group; + deferred.resolve(item); + }); + + // If not in the cache, and a root group, return + // the premade instance + } else if (rootGroups.get(item.group)) { + item.group = rootGroups.get(item.group); + deferred.resolve(item); + + // If not in the cache or a root group, fetch the parent + } else { + cache.set(item.group, defer()); + fetchItem(item.group).then(group => { + return construct(group, cache, true); + }).then(group => { + item.group = group; + deferred.resolve(item); + }, deferred.reject); + } + + return deferred.promise; +} + +function instantiate (object) { + if (object.type === 'bookmark') + return Bookmark(object); + if (object.type === 'group') + return Group(object); + if (object.type === 'separator') + return Separator(object); + return null; +} + +/** + * Validates a bookmark item; will throw an error if ininvalid, + * to be used with `promised`. As bookmark items check on their class, + * this only checks ducktypes + */ +function validate (object) { + if (!isDuckType(object)) return true; + let contract = object.type === 'bookmark' ? bookmarkContract : + object.type === 'group' ? groupContract : + object.type === 'separator' ? separatorContract : + null; + if (!contract) { + throw Error('No type specified'); + } + + // If object has a property set, and undefined, + // manually override with default as it'll fail otherwise + let withDefaults = Object.keys(defaults).reduce((obj, prop) => { + if (obj[prop] == null) obj[prop] = defaults[prop]; + return obj; + }, extend(object)); + + contract(withDefaults); +} + +function isDuckType (item) { + return !(item instanceof Bookmark) && + !(item instanceof Group) && + !(item instanceof Separator); +} + +function saveId (unsaved, id) { + itemMap.set(unsaved, id); +} + +// Fetches an item's ID from itself, or from the mapped items +function getId (item) { + return typeof item === 'number' ? item : + item ? item.id || itemMap.get(item) : + null; +} + +/* + * Set up the default, root groups + */ + +var defaultGroupMap = { + MENU: bmsrv.bookmarksMenuFolder, + TOOLBAR: bmsrv.toolbarFolder, + UNSORTED: bmsrv.unfiledBookmarksFolder +}; + +var rootGroups = new Map(); + +for (let i in defaultGroupMap) { + let group = Object.freeze(Group({ title: i, id: defaultGroupMap[i] })); + rootGroups.set(defaultGroupMap[i], group); + exports[i] = group; +} + +var defaults = { + group: exports.UNSORTED, + index: -1 +}; diff --git a/addon-sdk/source/lib/sdk/places/contract.js b/addon-sdk/source/lib/sdk/places/contract.js new file mode 100644 index 000000000..a3541c34d --- /dev/null +++ b/addon-sdk/source/lib/sdk/places/contract.js @@ -0,0 +1,73 @@ +/* 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 { isValidURI, URL } = require('../url'); +const { contract } = require('../util/contract'); +const { extend } = require('../util/object'); + +// map of property validations +const validItem = { + id: { + is: ['number', 'undefined', 'null'], + }, + group: { + is: ['object', 'number', 'undefined', 'null'], + ok: function (value) { + return value && + (value.toString && value.toString() === '[object Group]') || + typeof value === 'number' || + value.type === 'group'; + }, + msg: 'The `group` property must be a valid Group object' + }, + index: { + is: ['undefined', 'null', 'number'], + map: value => value == null ? -1 : value, + msg: 'The `index` property must be a number.' + }, + updated: { + is: ['number', 'undefined'] + } +}; + +const validTitle = { + title: { + is: ['string'], + msg: 'The `title` property must be defined.' + } +}; + +const validURL = { + url: { + is: ['string'], + ok: isValidURI, + msg: 'The `url` property must be a valid URL.' + } +}; + +const validTags = { + tags: { + is: ['object'], + ok: tags => tags instanceof Set, + map: function (tags) { + if (Array.isArray(tags)) + return new Set(tags); + if (tags == null) + return new Set(); + return tags; + }, + msg: 'The `tags` property must be a Set, or an array' + } +}; + +exports.bookmarkContract = contract( + extend(validItem, validTitle, validURL, validTags)); +exports.separatorContract = contract(validItem); +exports.groupContract = contract(extend(validItem, validTitle)); diff --git a/addon-sdk/source/lib/sdk/places/events.js b/addon-sdk/source/lib/sdk/places/events.js new file mode 100644 index 000000000..a3f95ee03 --- /dev/null +++ b/addon-sdk/source/lib/sdk/places/events.js @@ -0,0 +1,128 @@ +/* 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', + 'engines': { + 'Firefox': '*', + "SeaMonkey": '*' + } +}; + +const { Cc, Ci } = require('chrome'); +const { Unknown } = require('../platform/xpcom'); +const { Class } = require('../core/heritage'); +const { merge } = require('../util/object'); +const bookmarkService = Cc['@mozilla.org/browser/nav-bookmarks-service;1'] + .getService(Ci.nsINavBookmarksService); +const historyService = Cc['@mozilla.org/browser/nav-history-service;1'] + .getService(Ci.nsINavHistoryService); +const { mapBookmarkItemType } = require('./utils'); +const { EventTarget } = require('../event/target'); +const { emit } = require('../event/core'); +const { when } = require('../system/unload'); + +const emitter = EventTarget(); + +var HISTORY_ARGS = { + onBeginUpdateBatch: [], + onEndUpdateBatch: [], + onClearHistory: [], + onDeleteURI: ['url'], + onDeleteVisits: ['url', 'visitTime'], + onPageChanged: ['url', 'property', 'value'], + onTitleChanged: ['url', 'title'], + onVisit: [ + 'url', 'visitId', 'time', 'sessionId', 'referringId', 'transitionType' + ] +}; + +var HISTORY_EVENTS = { + onBeginUpdateBatch: 'history-start-batch', + onEndUpdateBatch: 'history-end-batch', + onClearHistory: 'history-start-clear', + onDeleteURI: 'history-delete-url', + onDeleteVisits: 'history-delete-visits', + onPageChanged: 'history-page-changed', + onTitleChanged: 'history-title-changed', + onVisit: 'history-visit' +}; + +var BOOKMARK_ARGS = { + onItemAdded: [ + 'id', 'parentId', 'index', 'type', 'url', 'title', 'dateAdded' + ], + onItemChanged: [ + 'id', 'property', null, 'value', 'lastModified', 'type', 'parentId' + ], + onItemMoved: [ + 'id', 'previousParentId', 'previousIndex', 'currentParentId', + 'currentIndex', 'type' + ], + onItemRemoved: ['id', 'parentId', 'index', 'type', 'url'], + onItemVisited: ['id', 'visitId', 'time', 'transitionType', 'url', 'parentId'] +}; + +var BOOKMARK_EVENTS = { + onItemAdded: 'bookmark-item-added', + onItemChanged: 'bookmark-item-changed', + onItemMoved: 'bookmark-item-moved', + onItemRemoved: 'bookmark-item-removed', + onItemVisited: 'bookmark-item-visited', +}; + +function createHandler (type, propNames) { + propNames = propNames || []; + return function (...args) { + let data = propNames.reduce((acc, prop, i) => { + if (prop) + acc[prop] = formatValue(prop, args[i]); + return acc; + }, {}); + + emit(emitter, 'data', { + type: type, + data: data + }); + }; +} + +/* + * Creates an observer, creating handlers based off of + * the `events` names, and ordering arguments from `propNames` hash + */ +function createObserverInstance (events, propNames) { + let definition = Object.keys(events).reduce((prototype, eventName) => { + prototype[eventName] = createHandler(events[eventName], propNames[eventName]); + return prototype; + }, {}); + + return Class(merge(definition, { extends: Unknown }))(); +} + +/* + * Formats `data` based off of the value of `type` + */ +function formatValue (type, data) { + if (type === 'type') + return mapBookmarkItemType(data); + if (type === 'url' && data) + return data.spec; + return data; +} + +var historyObserver = createObserverInstance(HISTORY_EVENTS, HISTORY_ARGS); +historyService.addObserver(historyObserver, false); + +var bookmarkObserver = createObserverInstance(BOOKMARK_EVENTS, BOOKMARK_ARGS); +bookmarkService.addObserver(bookmarkObserver, false); + +when(() => { + historyService.removeObserver(historyObserver); + bookmarkService.removeObserver(bookmarkObserver); +}); + +exports.events = emitter; diff --git a/addon-sdk/source/lib/sdk/places/favicon.js b/addon-sdk/source/lib/sdk/places/favicon.js new file mode 100644 index 000000000..05b057db1 --- /dev/null +++ b/addon-sdk/source/lib/sdk/places/favicon.js @@ -0,0 +1,49 @@ +/* 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", + "engines": { + "Firefox": "*", + "SeaMonkey": "*" + } +}; + +const { Cc, Ci, Cu } = require("chrome"); +const { defer, reject } = require("../core/promise"); +const FaviconService = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); +const AsyncFavicons = FaviconService.QueryInterface(Ci.mozIAsyncFavicons); +const { isValidURI } = require("../url"); +const { newURI, getURL } = require("../url/utils"); + +/** + * Takes an object of several possible types and + * returns a promise that resolves to the page's favicon URI. + * @param {String|Tab} object + * @param {Function} (callback) + * @returns {Promise} + */ + +function getFavicon (object, callback) { + let url = getURL(object); + let deferred = defer(); + + if (url && isValidURI(url)) { + AsyncFavicons.getFaviconURLForPage(newURI(url), function (aURI) { + if (aURI && aURI.spec) + deferred.resolve(aURI.spec.toString()); + else + deferred.reject(null); + }); + } else { + deferred.reject(null); + } + + if (callback) deferred.promise.then(callback, callback); + return deferred.promise; +} +exports.getFavicon = getFavicon; diff --git a/addon-sdk/source/lib/sdk/places/history.js b/addon-sdk/source/lib/sdk/places/history.js new file mode 100644 index 000000000..b243b024c --- /dev/null +++ b/addon-sdk/source/lib/sdk/places/history.js @@ -0,0 +1,65 @@ +/* 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", + "engines": { + "Firefox": "*", + "SeaMonkey": "*" + } +}; + +/* + * Requiring hosts so they can subscribe to client messages + */ +require('./host/host-bookmarks'); +require('./host/host-tags'); +require('./host/host-query'); + +const { Cc, Ci } = require('chrome'); +const { Class } = require('../core/heritage'); +const { events, send } = require('../addon/events'); +const { defer, reject, all } = require('../core/promise'); +const { uuid } = require('../util/uuid'); +const { flatten } = require('../util/array'); +const { has, extend, merge, pick } = require('../util/object'); +const { emit } = require('../event/core'); +const { defer: async } = require('../lang/functional'); +const { EventTarget } = require('../event/target'); +const { + urlQueryParser, createQuery, createQueryOptions +} = require('./utils'); + +/* + * Constant used by nsIHistoryQuery; 0 is a history query + * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions + */ +const HISTORY_QUERY = 0; + +var search = function query (queries, options) { + queries = [].concat(queries); + let emitter = EventTarget(); + let queryObjs = queries.map(createQuery.bind(null, HISTORY_QUERY)); + let optionsObj = createQueryOptions(HISTORY_QUERY, options); + + // Can remove after `Promise.jsm` is implemented in Bug 881047, + // which will guarantee next tick execution + async(() => { + send('sdk-places-query', { + query: queryObjs, + options: optionsObj + }).then(results => { + results.map(item => emit(emitter, 'data', item)); + emit(emitter, 'end', results); + }, reason => { + emit(emitter, 'error', reason); + emit(emitter, 'end', []); + }); + })(); + + return emitter; +}; +exports.search = search; diff --git a/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js b/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js new file mode 100644 index 000000000..3245c4070 --- /dev/null +++ b/addon-sdk/source/lib/sdk/places/host/host-bookmarks.js @@ -0,0 +1,238 @@ +/* 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", + "engines": { + "Firefox": "*", + "SeaMonkey": "*" + } +}; + +const { Cc, Ci } = require('chrome'); +const browserHistory = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsIBrowserHistory); +const asyncHistory = Cc["@mozilla.org/browser/history;1"]. + getService(Ci.mozIAsyncHistory); +const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); +const taggingService = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); +const ios = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); +const { query } = require('./host-query'); +const { + defer, all, resolve, promised, reject +} = require('../../core/promise'); +const { request, response } = require('../../addon/host'); +const { send } = require('../../addon/events'); +const { on, emit } = require('../../event/core'); +const { filter } = require('../../event/utils'); +const { URL, isValidURI } = require('../../url'); +const { newURI } = require('../../url/utils'); + +const DEFAULT_INDEX = bmsrv.DEFAULT_INDEX; +const UNSORTED_ID = bmsrv.unfiledBookmarksFolder; +const ROOT_FOLDERS = [ + bmsrv.unfiledBookmarksFolder, bmsrv.toolbarFolder, + bmsrv.tagsFolder, bmsrv.bookmarksMenuFolder +]; + +const EVENT_MAP = { + 'sdk-places-bookmarks-create': createBookmarkItem, + 'sdk-places-bookmarks-save': saveBookmarkItem, + 'sdk-places-bookmarks-last-updated': getBookmarkLastUpdated, + 'sdk-places-bookmarks-get': getBookmarkItem, + 'sdk-places-bookmarks-remove': removeBookmarkItem, + 'sdk-places-bookmarks-get-all': getAllBookmarks, + 'sdk-places-bookmarks-get-children': getChildren +}; + +function typeMap (type) { + if (typeof type === 'number') { + if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark'; + if (bmsrv.TYPE_FOLDER === type) return 'group'; + if (bmsrv.TYPE_SEPARATOR === type) return 'separator'; + } else { + if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK; + if ('group' === type) return bmsrv.TYPE_FOLDER; + if ('separator' === type) return bmsrv.TYPE_SEPARATOR; + } +} + +function getBookmarkLastUpdated ({id}) { + return resolve(bmsrv.getItemLastModified(id)); +} +exports.getBookmarkLastUpdated; + +function createBookmarkItem (data) { + let error; + + if (data.group == null) data.group = UNSORTED_ID; + if (data.index == null) data.index = DEFAULT_INDEX; + + if (data.type === 'group') + data.id = bmsrv.createFolder( + data.group, data.title, data.index + ); + else if (data.type === 'separator') + data.id = bmsrv.insertSeparator( + data.group, data.index + ); + else + data.id = bmsrv.insertBookmark( + data.group, newURI(data.url), data.index, data.title + ); + + // In the event where default or no index is provided (-1), + // query the actual index for the response + if (data.index === -1) + data.index = bmsrv.getItemIndex(data.id); + + try { + data.updated = bmsrv.getItemLastModified(data.id); + } + catch (e) { + console.exception(e); + } + + return tag(data, true).then(() => data); +} +exports.createBookmarkItem = createBookmarkItem; + +function saveBookmarkItem (data) { + let id = data.id; + if (!id) + reject('Item is missing id'); + + let group = bmsrv.getFolderIdForItem(id); + let index = bmsrv.getItemIndex(id); + let type = bmsrv.getItemType(id); + let title = typeMap(type) !== 'separator' ? + bmsrv.getItemTitle(id) : + undefined; + let url = typeMap(type) === 'bookmark' ? + bmsrv.getBookmarkURI(id).spec : + undefined; + + if (url != data.url) + bmsrv.changeBookmarkURI(id, newURI(data.url)); + else if (typeMap(type) === 'bookmark') + data.url = url; + + if (title != data.title) + bmsrv.setItemTitle(id, data.title); + else if (typeMap(type) !== 'separator') + data.title = title; + + if (data.group && data.group !== group) + bmsrv.moveItem(id, data.group, data.index || -1); + else if (data.index != null && data.index !== index) { + // We use moveItem here instead of setItemIndex + // so we don't have to manage the indicies of the siblings + bmsrv.moveItem(id, group, data.index); + } else if (data.index == null) + data.index = index; + + data.updated = bmsrv.getItemLastModified(data.id); + + return tag(data).then(() => data); +} +exports.saveBookmarkItem = saveBookmarkItem; + +function removeBookmarkItem (data) { + let id = data.id; + + if (!id) + reject('Item is missing id'); + + bmsrv.removeItem(id); + return resolve(null); +} +exports.removeBookmarkItem = removeBookmarkItem; + +function getBookmarkItem (data) { + let id = data.id; + + if (!id) + reject('Item is missing id'); + + let type = bmsrv.getItemType(id); + + data.type = typeMap(type); + + if (type === bmsrv.TYPE_BOOKMARK || type === bmsrv.TYPE_FOLDER) + data.title = bmsrv.getItemTitle(id); + + if (type === bmsrv.TYPE_BOOKMARK) { + data.url = bmsrv.getBookmarkURI(id).spec; + // Should be moved into host-tags as a method + data.tags = taggingService.getTagsForURI(newURI(data.url), {}); + } + + data.group = bmsrv.getFolderIdForItem(id); + data.index = bmsrv.getItemIndex(id); + data.updated = bmsrv.getItemLastModified(data.id); + + return resolve(data); +} +exports.getBookmarkItem = getBookmarkItem; + +function getAllBookmarks () { + return query({}, { queryType: 1 }).then(bookmarks => + all(bookmarks.map(getBookmarkItem))); +} +exports.getAllBookmarks = getAllBookmarks; + +function getChildren ({ id }) { + if (typeMap(bmsrv.getItemType(id)) !== 'group') return []; + let ids = []; + for (let i = 0; ids[ids.length - 1] !== -1; i++) + ids.push(bmsrv.getIdForItemAt(id, i)); + ids.pop(); + return all(ids.map(id => getBookmarkItem({ id: id }))); +} +exports.getChildren = getChildren; + +/* + * Hook into host + */ + +var reqStream = filter(request, (data) => /sdk-places-bookmarks/.test(data.event)); +on(reqStream, 'data', ({ event, id, data }) => { + if (!EVENT_MAP[event]) return; + + let resData = { id: id, event: event }; + + promised(EVENT_MAP[event])(data). + then(res => resData.data = res, e => resData.error = e). + then(() => emit(response, 'data', resData)); +}); + +function tag (data, isNew) { + // If a new item, we can skip checking what other tags + // are on the item + if (data.type !== 'bookmark') { + return resolve(); + } + else if (!isNew) { + return send('sdk-places-tags-get-tags-by-url', { url: data.url }) + .then(tags => { + return send('sdk-places-tags-untag', { + tags: tags.filter(tag => !~data.tags.indexOf(tag)), + url: data.url + }); + }).then(() => send('sdk-places-tags-tag', { + url: data.url, tags: data.tags + })); + } + else if (data.tags && data.tags.length) { + return send('sdk-places-tags-tag', { url: data.url, tags: data.tags }); + } + else + return resolve(); +} + diff --git a/addon-sdk/source/lib/sdk/places/host/host-query.js b/addon-sdk/source/lib/sdk/places/host/host-query.js new file mode 100644 index 000000000..f2dbd6550 --- /dev/null +++ b/addon-sdk/source/lib/sdk/places/host/host-query.js @@ -0,0 +1,179 @@ +/* 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", + "engines": { + "Firefox": "*", + "SeaMonkey": "*" + } +}; + +const { Cc, Ci } = require('chrome'); +const { all } = require('../../core/promise'); +const { safeMerge, omit } = require('../../util/object'); +const historyService = Cc['@mozilla.org/browser/nav-history-service;1'] + .getService(Ci.nsINavHistoryService); +const bookmarksService = Cc['@mozilla.org/browser/nav-bookmarks-service;1'] + .getService(Ci.nsINavBookmarksService); +const { request, response } = require('../../addon/host'); +const { newURI } = require('../../url/utils'); +const { send } = require('../../addon/events'); +const { on, emit } = require('../../event/core'); +const { filter } = require('../../event/utils'); + +const ROOT_FOLDERS = [ + bookmarksService.unfiledBookmarksFolder, bookmarksService.toolbarFolder, + bookmarksService.bookmarksMenuFolder +]; + +const EVENT_MAP = { + 'sdk-places-query': queryReceiver +}; + +// Properties that need to be manually +// copied into a nsINavHistoryQuery object +const MANUAL_QUERY_PROPERTIES = [ + 'uri', 'folder', 'tags', 'url', 'folder' +]; + +const PLACES_PROPERTIES = [ + 'uri', 'title', 'accessCount', 'time' +]; + +function execute (queries, options) { + return new Promise(resolve => { + let root = historyService + .executeQueries(queries, queries.length, options).root; + // Let's extract an eventual uri wildcard, if both domain and uri are set. + // See utils.js::urlQueryParser() for more details. + // In case of multiple queries, we only retain the first found wildcard. + let uriWildcard = queries.reduce((prev, query) => { + if (query.uri && query.domain) { + if (!prev) + prev = query.uri.spec; + query.uri = null; + } + return prev; + }, ""); + resolve(collect([], root, uriWildcard)); + }); +} + +function collect (acc, node, uriWildcard) { + node.containerOpen = true; + for (let i = 0; i < node.childCount; i++) { + let child = node.getChild(i); + + if (!uriWildcard || child.uri.startsWith(uriWildcard)) { + acc.push(child); + } + if (child.type === child.RESULT_TYPE_FOLDER) { + let container = child.QueryInterface(Ci.nsINavHistoryContainerResultNode); + collect(acc, container, uriWildcard); + } + } + node.containerOpen = false; + return acc; +} + +function query (queries, options) { + return new Promise((resolve, reject) => { + queries = queries || []; + options = options || {}; + let optionsObj, queryObjs; + + optionsObj = historyService.getNewQueryOptions(); + queryObjs = [].concat(queries).map(createQuery); + if (!queryObjs.length) { + queryObjs = [historyService.getNewQuery()]; + } + safeMerge(optionsObj, options); + + /* + * Currently `places:` queries are not supported + */ + optionsObj.excludeQueries = true; + + execute(queryObjs, optionsObj).then((results) => { + if (optionsObj.queryType === 0) { + return results.map(normalize); + } + else if (optionsObj.queryType === 1) { + // Formats query results into more standard + // data structures for returning + return all(results.map(({itemId}) => + send('sdk-places-bookmarks-get', { id: itemId }))); + } + }).then(resolve, reject); + }); +} +exports.query = query; + +function createQuery (query) { + query = query || {}; + let queryObj = historyService.getNewQuery(); + + safeMerge(queryObj, omit(query, MANUAL_QUERY_PROPERTIES)); + + if (query.tags && Array.isArray(query.tags)) + queryObj.tags = query.tags; + if (query.uri || query.url) + queryObj.uri = newURI(query.uri || query.url); + if (query.folder) + queryObj.setFolders([query.folder], 1); + return queryObj; +} + +function queryReceiver (message) { + let queries = message.data.queries || message.data.query; + let options = message.data.options; + let resData = { + id: message.id, + event: message.event + }; + + query(queries, options).then(results => { + resData.data = results; + respond(resData); + }, reason => { + resData.error = reason; + respond(resData); + }); +} + +/* + * Converts a nsINavHistoryResultNode into a plain object + * + * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode + */ +function normalize (historyObj) { + return PLACES_PROPERTIES.reduce((obj, prop) => { + if (prop === 'uri') + obj.url = historyObj.uri; + else if (prop === 'time') { + // Cast from microseconds to milliseconds + obj.time = Math.floor(historyObj.time / 1000) + } + else if (prop === 'accessCount') + obj.visitCount = historyObj[prop]; + else + obj[prop] = historyObj[prop]; + return obj; + }, {}); +} + +/* + * Hook into host + */ + +var reqStream = filter(request, data => /sdk-places-query/.test(data.event)); +on(reqStream, 'data', function (e) { + if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e); +}); + +function respond (data) { + emit(response, 'data', data); +} diff --git a/addon-sdk/source/lib/sdk/places/host/host-tags.js b/addon-sdk/source/lib/sdk/places/host/host-tags.js new file mode 100644 index 000000000..929a5d5af --- /dev/null +++ b/addon-sdk/source/lib/sdk/places/host/host-tags.js @@ -0,0 +1,92 @@ +/* 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", + "engines": { + "Firefox": "*", + "SeaMonkey": "*" + } +}; + +const { Cc, Ci } = require('chrome'); +const taggingService = Cc["@mozilla.org/browser/tagging-service;1"]. + getService(Ci.nsITaggingService); +const ios = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); +const { URL } = require('../../url'); +const { newURI } = require('../../url/utils'); +const { request, response } = require('../../addon/host'); +const { on, emit } = require('../../event/core'); +const { filter } = require('../../event/utils'); + +const EVENT_MAP = { + 'sdk-places-tags-tag': tag, + 'sdk-places-tags-untag': untag, + 'sdk-places-tags-get-tags-by-url': getTagsByURL, + 'sdk-places-tags-get-urls-by-tag': getURLsByTag +}; + +function tag (message) { + let data = message.data; + let resData = { + id: message.id, + event: message.event + }; + + resData.data = taggingService.tagURI(newURI(data.url), data.tags); + respond(resData); +} + +function untag (message) { + let data = message.data; + let resData = { + id: message.id, + event: message.event + }; + + resData.data = taggingService.untagURI(newURI(data.url), data.tags); + respond(resData); +} + +function getURLsByTag (message) { + let data = message.data; + let resData = { + id: message.id, + event: message.event + }; + + resData.data = taggingService + .getURIsForTag(data.tag).map(uri => uri.spec); + respond(resData); +} + +function getTagsByURL (message) { + let data = message.data; + let resData = { + id: message.id, + event: message.event + }; + + resData.data = taggingService.getTagsForURI(newURI(data.url), {}); + respond(resData); +} + +/* + * Hook into host + */ + +var reqStream = filter(request, function (data) { + return /sdk-places-tags/.test(data.event); +}); + +on(reqStream, 'data', function (e) { + if (EVENT_MAP[e.event]) EVENT_MAP[e.event](e); +}); + +function respond (data) { + emit(response, 'data', data); +} diff --git a/addon-sdk/source/lib/sdk/places/utils.js b/addon-sdk/source/lib/sdk/places/utils.js new file mode 100644 index 000000000..44366d2aa --- /dev/null +++ b/addon-sdk/source/lib/sdk/places/utils.js @@ -0,0 +1,268 @@ +/* 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", + "engines": { + "Firefox": "*", + "SeaMonkey": "*" + } +}; + +const { Cc, Ci, Cu } = require('chrome'); +const { Class } = require('../core/heritage'); +const { method } = require('../lang/functional'); +const { defer, promised, all } = require('../core/promise'); +const { send } = require('../addon/events'); +const { EventTarget } = require('../event/target'); +const { merge } = require('../util/object'); +const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. + getService(Ci.nsINavBookmarksService); + +Cu.importGlobalProperties(["URL"]); + +/* + * TreeNodes are used to construct dependency trees + * for BookmarkItems + */ +var TreeNode = Class({ + initialize: function (value) { + this.value = value; + this.children = []; + }, + add: function (values) { + [].concat(values).forEach(value => { + this.children.push(value instanceof TreeNode ? value : TreeNode(value)); + }); + }, + get length () { + let count = 0; + this.walk(() => count++); + // Do not count the current node + return --count; + }, + get: method(get), + walk: method(walk), + toString: () => '[object TreeNode]' +}); +exports.TreeNode = TreeNode; + +/* + * Descends down from `node` applying `fn` to each in order. + * `fn` can return values or promises -- if promise returned, + * children are not processed until resolved. `fn` is passed + * one argument, the current node, `curr`. + */ +function walk (curr, fn) { + return promised(fn)(curr).then(val => { + return all(curr.children.map(child => walk(child, fn))); + }); +} + +/* + * Descends from the TreeNode `node`, returning + * the node with value `value` if found or `null` + * otherwise + */ +function get (node, value) { + if (node.value === value) return node; + for (let child of node.children) { + let found = get(child, value); + if (found) return found; + } + return null; +} + +/* + * Constructs a tree of bookmark nodes + * returning the root (value: null); + */ + +function constructTree (items) { + let root = TreeNode(null); + items.forEach(treeify.bind(null, root)); + + function treeify (root, item) { + // If node already exists, skip + let node = root.get(item); + if (node) return node; + node = TreeNode(item); + + let parentNode = item.group ? treeify(root, item.group) : root; + parentNode.add(node); + + return node; + } + + return root; +} +exports.constructTree = constructTree; + +/* + * Shortcut for converting an id, or an object with an id, into + * an object with corresponding bookmark data + */ +function fetchItem (item) { + return send('sdk-places-bookmarks-get', { id: item.id || item }); +} +exports.fetchItem = fetchItem; + +/* + * Takes an ID or an object with ID and checks it against + * the root bookmark folders + */ +function isRootGroup (id) { + id = id && id.id; + return ~[bmsrv.bookmarksMenuFolder, bmsrv.toolbarFolder, + bmsrv.unfiledBookmarksFolder + ].indexOf(id); +} +exports.isRootGroup = isRootGroup; + +/* + * Merges appropriate options into query based off of url + * 4 scenarios: + * + * 'moz.com' // domain: moz.com, domainIsHost: true + * --> 'http://moz.com', 'http://moz.com/thunderbird' + * '*.moz.com' // domain: moz.com, domainIsHost: false + * --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test' + * 'http://moz.com' // uri: http://moz.com/ + * --> 'http://moz.com/' + * 'http://moz.com/*' // uri: http://moz.com/, domain: moz.com, domainIsHost: true + * --> 'http://moz.com/', 'http://moz.com/thunderbird' + */ + +function urlQueryParser (query, url) { + if (!url) return; + if (/^https?:\/\//.test(url)) { + query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/'; + if (/\*$/.test(url)) { + // Wildcard searches on URIs are not supported, so try to extract a + // domain and filter the data later. + url = url.replace(/\*$/, ''); + try { + query.domain = new URL(url).hostname; + query.domainIsHost = true; + // Unfortunately here we cannot use an expando to store the wildcard, + // cause the query is a wrapped native XPCOM object, so we reuse uri. + // We clearly don't want to query for both uri and domain, thus we'll + // have to handle this in host-query.js::execute() + query.uri = url; + } catch (ex) { + // Cannot extract an host cause it's not a valid uri, the query will + // just return nothing. + } + } + } else { + if (/^\*/.test(url)) { + query.domain = url.replace(/^\*\./, ''); + query.domainIsHost = false; + } else { + query.domain = url; + query.domainIsHost = true; + } + } +} +exports.urlQueryParser = urlQueryParser; + +/* + * Takes an EventEmitter and returns a promise that + * aggregates results and handles a bulk resolve and reject + */ + +function promisedEmitter (emitter) { + let { promise, resolve, reject } = defer(); + let errors = []; + emitter.on('error', error => errors.push(error)); + emitter.on('end', (items) => { + if (errors.length) reject(errors[0]); + else resolve(items); + }); + return promise; +} +exports.promisedEmitter = promisedEmitter; + + +// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions +function createQuery (type, query) { + query = query || {}; + let qObj = { + searchTerms: query.query + }; + + urlQueryParser(qObj, query.url); + + // 0 === history + if (type === 0) { + // PRTime used by query is in microseconds, not milliseconds + qObj.beginTime = (query.from || 0) * 1000; + qObj.endTime = (query.to || new Date()) * 1000; + + // Set reference time to Epoch + qObj.beginTimeReference = 0; + qObj.endTimeReference = 0; + } + // 1 === bookmarks + else if (type === 1) { + qObj.tags = query.tags; + qObj.folder = query.group && query.group.id; + } + // 2 === unified (not implemented on platform) + else if (type === 2) { + + } + + return qObj; +} +exports.createQuery = createQuery; + +// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions + +const SORT_MAP = { + title: 1, + date: 3, // sort by visit date + url: 5, + visitCount: 7, + // keywords currently unsupported + // keyword: 9, + dateAdded: 11, // bookmarks only + lastModified: 13 // bookmarks only +}; + +function createQueryOptions (type, options) { + options = options || {}; + let oObj = {}; + oObj.sortingMode = SORT_MAP[options.sort] || 0; + if (options.descending && options.sort) + oObj.sortingMode++; + + // Resolve to default sort if ineligible based on query type + if (type === 0 && // history + (options.sort === 'dateAdded' || options.sort === 'lastModified')) + oObj.sortingMode = 0; + + oObj.maxResults = typeof options.count === 'number' ? options.count : 0; + + oObj.queryType = type; + + return oObj; +} +exports.createQueryOptions = createQueryOptions; + + +function mapBookmarkItemType (type) { + if (typeof type === 'number') { + if (bmsrv.TYPE_BOOKMARK === type) return 'bookmark'; + if (bmsrv.TYPE_FOLDER === type) return 'group'; + if (bmsrv.TYPE_SEPARATOR === type) return 'separator'; + } else { + if ('bookmark' === type) return bmsrv.TYPE_BOOKMARK; + if ('group' === type) return bmsrv.TYPE_FOLDER; + if ('separator' === type) return bmsrv.TYPE_SEPARATOR; + } +} +exports.mapBookmarkItemType = mapBookmarkItemType; diff --git a/addon-sdk/source/lib/sdk/platform/xpcom.js b/addon-sdk/source/lib/sdk/platform/xpcom.js new file mode 100644 index 000000000..383baf67a --- /dev/null +++ b/addon-sdk/source/lib/sdk/platform/xpcom.js @@ -0,0 +1,241 @@ +/* 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, Cr, Cm, components: { classesByID } } = require('chrome'); +const { registerFactory, unregisterFactory, isCIDRegistered } = + Cm.QueryInterface(Ci.nsIComponentRegistrar); + +const { merge } = require('../util/object'); +const { Class, extend, mix } = require('../core/heritage'); +const { uuid } = require('../util/uuid'); + +// This is a base prototype, that provides bare bones of XPCOM. JS based +// components can be easily implement by extending it. +const Unknown = new function() { + function hasInterface(component, iid) { + return component && component.interfaces && + ( component.interfaces.some(id => iid.equals(Ci[id])) || + component.implements.some($ => hasInterface($, iid)) || + hasInterface(Object.getPrototypeOf(component), iid)); + } + + return Class({ + /** + * The `QueryInterface` method provides runtime type discovery used by XPCOM. + * This method return queried instance of `this` if given `iid` is listed in + * the `interfaces` property or in equivalent properties of objects in it's + * prototype chain. In addition it will look up in the prototypes under + * `implements` array property, this ways compositions made via `Class` + * utility will carry interfaces implemented by composition components. + */ + QueryInterface: function QueryInterface(iid) { + // For some reason there are cases when `iid` is `null`. In such cases we + // just return `this`. Otherwise we verify that component implements given + // `iid` interface. This will be no longer necessary once Bug 748003 is + // fixed. + if (iid && !hasInterface(this, iid)) + throw Cr.NS_ERROR_NO_INTERFACE; + + return this; + }, + /** + * Array of `XPCOM` interfaces (as strings) implemented by this component. + * All components implement `nsISupports` by default which is default value + * here. Provide array of interfaces implemented by an object when + * extending, to append them to this list (Please note that there is no + * need to repeat interfaces implemented by super as they will be added + * automatically). + */ + interfaces: Object.freeze([ 'nsISupports' ]) + }); +} +exports.Unknown = Unknown; + +// Base exemplar for creating instances implementing `nsIFactory` interface, +// that maybe registered into runtime via `register` function. Instances of +// this factory create instances of enclosed component on `createInstance`. +const Factory = Class({ + extends: Unknown, + interfaces: [ 'nsIFactory' ], + /** + * All the descendants will get auto generated `id` (also known as `classID` + * in XPCOM world) unless one is manually provided. + */ + get id() { throw Error('Factory must implement `id` property') }, + /** + * XPCOM `contractID` may optionally be provided to associate this factory + * with it. `contract` is a unique string that has a following format: + * '@vendor.com/unique/id;1'. + */ + contract: null, + /** + * Class description that is being registered. This value is intended as a + * human-readable description for the given class and does not needs to be + * globally unique. + */ + description: 'Jetpack generated factory', + /** + * This method is required by `nsIFactory` interfaces, but as in most + * implementations it does nothing interesting. + */ + lockFactory: function lockFactory(lock) { + return undefined; + }, + /** + * If property is `true` XPCOM service / factory will be registered + * automatically on creation. + */ + register: true, + /** + * If property is `true` XPCOM factory will be unregistered prior to add-on + * unload. + */ + unregister: true, + /** + * Method is called on `Service.new(options)` passing given `options` to + * it. Options is expected to have `component` property holding XPCOM + * component implementation typically decedent of `Unknown` or any custom + * implementation with a `new` method and optional `register`, `unregister` + * flags. Unless `register` is `false` Service / Factory will be + * automatically registered. Unless `unregister` is `false` component will + * be automatically unregistered on add-on unload. + */ + initialize: function initialize(options) { + merge(this, { + id: 'id' in options ? options.id : uuid(), + register: 'register' in options ? options.register : this.register, + unregister: 'unregister' in options ? options.unregister : this.unregister, + contract: 'contract' in options ? options.contract : null, + Component: options.Component + }); + + // If service / factory has auto registration enabled then register. + if (this.register) + register(this); + }, + /** + * Creates an instance of the class associated with this factory. + */ + createInstance: function createInstance(outer, iid) { + try { + if (outer) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.create().QueryInterface(iid); + } + catch (error) { + throw error instanceof Ci.nsIException ? error : Cr.NS_ERROR_FAILURE; + } + }, + create: function create() { + return this.Component(); + } +}); +exports.Factory = Factory; + +// Exemplar for creating services that implement `nsIFactory` interface, that +// can be registered into runtime via call to `register`. This services return +// enclosed `component` on `getService`. +const Service = Class({ + extends: Factory, + initialize: function initialize(options) { + this.component = options.Component(); + Factory.prototype.initialize.call(this, options); + }, + description: 'Jetpack generated service', + /** + * Creates an instance of the class associated with this factory. + */ + create: function create() { + return this.component; + } +}); +exports.Service = Service; + +function isRegistered({ id }) { + return isCIDRegistered(id); +} +exports.isRegistered = isRegistered; + +/** + * Registers given `component` object to be used to instantiate a particular + * class identified by `component.id`, and creates an association of class + * name and `component.contract` with the class. + */ +function register(factory) { + if (!(factory instanceof Factory)) { + throw new Error("xpcom.register() expect a Factory instance.\n" + + "Please refactor your code to new xpcom module if you" + + " are repacking an addon from SDK <= 1.5:\n" + + "https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/platform_xpcom"); + } + + registerFactory(factory.id, factory.description, factory.contract, factory); + + if (factory.unregister) + require('../system/unload').when(unregister.bind(null, factory)); +} +exports.register = register; + +/** + * Unregister a factory associated with a particular class identified by + * `factory.classID`. + */ +function unregister(factory) { + if (isRegistered(factory)) + unregisterFactory(factory.id, factory); +} +exports.unregister = unregister; + +function autoRegister(path) { + // TODO: This assumes that the url points to a directory + // that contains subdirectories corresponding to OS/ABI and then + // further subdirectories corresponding to Gecko platform version. + // we should probably either behave intelligently here or allow + // the caller to pass-in more options if e.g. there aren't + // Gecko-specific binaries for a component (which will be the case + // if only frozen interfaces are used). + + var runtime = require("../system/runtime"); + var osDirName = runtime.OS + "_" + runtime.XPCOMABI; + var platformVersion = require("../system/xul-app").platformVersion.substring(0, 5); + + var file = Cc['@mozilla.org/file/local;1'] + .createInstance(Ci.nsILocalFile); + file.initWithPath(path); + file.append(osDirName); + file.append(platformVersion); + + if (!(file.exists() && file.isDirectory())) + throw new Error("component not available for OS/ABI " + + osDirName + " and platform " + platformVersion); + + Cm.QueryInterface(Ci.nsIComponentRegistrar); + Cm.autoRegister(file); +} +exports.autoRegister = autoRegister; + +/** + * Returns registered factory that has a given `id` or `null` if not found. + */ +function factoryByID(id) { + return classesByID[id] || null; +} +exports.factoryByID = factoryByID; + +/** + * Returns factory registered with a given `contract` or `null` if not found. + * In contrast to `Cc[contract]` that does ignores new factory registration + * with a given `contract` this will return a factory currently associated + * with a `contract`. + */ +function factoryByContract(contract) { + return factoryByID(Cm.contractIDToCID(contract)); +} +exports.factoryByContract = factoryByContract; diff --git a/addon-sdk/source/lib/sdk/preferences/event-target.js b/addon-sdk/source/lib/sdk/preferences/event-target.js new file mode 100644 index 000000000..b64ba303c --- /dev/null +++ b/addon-sdk/source/lib/sdk/preferences/event-target.js @@ -0,0 +1,61 @@ +/* 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 { Class } = require('../core/heritage'); +const { EventTarget } = require('../event/target'); +const { Branch } = require('./service'); +const { emit, off } = require('../event/core'); +const { when: unload } = require('../system/unload'); + +const prefTargetNS = require('../core/namespace').ns(); + +const PrefsTarget = Class({ + extends: EventTarget, + initialize: function(options) { + options = options || {}; + EventTarget.prototype.initialize.call(this, options); + + let branchName = options.branchName || ''; + let branch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(branchName). + QueryInterface(Ci.nsIPrefBranch2); + prefTargetNS(this).branch = branch; + + // provides easy access to preference values + this.prefs = Branch(branchName); + + // start listening to preference changes + let observer = prefTargetNS(this).observer = onChange.bind(this); + branch.addObserver('', observer, false); + + // Make sure to destroy this on unload + unload(destroy.bind(this)); + } +}); +exports.PrefsTarget = PrefsTarget; + +/* HELPERS */ + +function onChange(subject, topic, name) { + if (topic === 'nsPref:changed') { + emit(this, name, name); + emit(this, '', name); + } +} + +function destroy() { + off(this); + + // stop listening to preference changes + let branch = prefTargetNS(this).branch; + branch.removeObserver('', prefTargetNS(this).observer, false); + prefTargetNS(this).observer = null; +} diff --git a/addon-sdk/source/lib/sdk/preferences/native-options.js b/addon-sdk/source/lib/sdk/preferences/native-options.js new file mode 100644 index 000000000..840997df9 --- /dev/null +++ b/addon-sdk/source/lib/sdk/preferences/native-options.js @@ -0,0 +1,193 @@ +/* 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, Cu } = require('chrome'); +const { on } = require('../system/events'); +const { id, preferencesBranch } = require('../self'); +const { localizeInlineOptions } = require('../l10n/prefs'); +const { Services } = require("resource://gre/modules/Services.jsm"); +const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm"); +const { defer } = require("sdk/core/promise"); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";; +const DEFAULT_OPTIONS_URL = 'data:text/xml,'; + +const VALID_PREF_TYPES = ['bool', 'boolint', 'integer', 'string', 'color', + 'file', 'directory', 'control', 'menulist', 'radio']; + +const isFennec = require("sdk/system/xul-app").is("Fennec"); + +function enable({ preferences, id }) { + let enabled = defer(); + + validate(preferences); + + setDefaults(preferences, preferencesBranch); + + // allow the use of custom options.xul + AddonManager.getAddonByID(id, (addon) => { + on('addon-options-displayed', onAddonOptionsDisplayed, true); + enabled.resolve({ id: id }); + }); + + function onAddonOptionsDisplayed({ subject: doc, data }) { + if (data === id) { + let parent; + + if (isFennec) { + parent = doc.querySelector('.options-box'); + + // NOTE: This disable the CSS rule that makes the options invisible + let item = doc.querySelector('#addons-details .addon-item'); + item.removeAttribute("optionsURL"); + } else { + parent = doc.getElementById('detail-downloads').parentNode; + } + + if (parent) { + injectOptions({ + preferences: preferences, + preferencesBranch: preferencesBranch, + document: doc, + parent: parent, + id: id + }); + localizeInlineOptions(doc); + } else { + throw Error("Preferences parent node not found in Addon Details. The configured custom preferences will not be visible."); + } + } + } + + return enabled.promise; +} +exports.enable = enable; + +// centralized sanity checks +function validate(preferences) { + for (let { name, title, type, label, options } of preferences) { + // make sure the title is set and non-empty + if (!title) + throw Error("The '" + name + "' pref requires a title"); + + // make sure that pref type is a valid inline option type + if (!~VALID_PREF_TYPES.indexOf(type)) + throw Error("The '" + name + "' pref must be of valid type"); + + // if it's a control, make sure it has a label + if (type === 'control' && !label) + throw Error("The '" + name + "' control requires a label"); + + // if it's a menulist or radio, make sure it has options + if (type === 'menulist' || type === 'radio') { + if (!options) + throw Error("The '" + name + "' pref requires options"); + + // make sure each option has a value and a label + for (let item of options) { + if (!('value' in item) || !('label' in item)) + throw Error("Each option requires both a value and a label"); + } + } + + // TODO: check that pref type matches default value type + } +} +exports.validate = validate; + +// initializes default preferences, emulates defaults/prefs.js +function setDefaults(preferences, preferencesBranch) { + const branch = Cc['@mozilla.org/preferences-service;1']. + getService(Ci.nsIPrefService). + getDefaultBranch('extensions.' + preferencesBranch + '.'); + for (let { name, value } of preferences) { + switch (typeof value) { + case 'boolean': + branch.setBoolPref(name, value); + break; + case 'number': + // must be integer, ignore otherwise + if (value % 1 === 0) { + branch.setIntPref(name, value); + } + break; + case 'string': + let str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + str.data = value; + branch.setComplexValue(name, Ci.nsISupportsString, str); + break; + } + } +} +exports.setDefaults = setDefaults; + +// dynamically injects inline options into about:addons page at runtime +// NOTE: on Firefox Desktop the about:addons page is a xul page document, +// on Firefox for Android the about:addons page is an xhtml page, to support both +// the XUL xml namespace have to be enforced. +function injectOptions({ preferences, preferencesBranch, document, parent, id }) { + preferences.forEach(({name, type, hidden, title, description, label, options, on, off}) => { + if (hidden) { + return; + } + + let setting = document.createElementNS(XUL_NS, 'setting'); + setting.setAttribute('pref-name', name); + setting.setAttribute('data-jetpack-id', id); + setting.setAttribute('pref', 'extensions.' + preferencesBranch + '.' + name); + setting.setAttribute('type', type); + setting.setAttribute('title', title); + if (description) + setting.setAttribute('desc', description); + + if (type === 'file' || type === 'directory') { + setting.setAttribute('fullpath', 'true'); + } + else if (type === 'control') { + let button = document.createElementNS(XUL_NS, 'button'); + button.setAttribute('pref-name', name); + button.setAttribute('data-jetpack-id', id); + button.setAttribute('label', label); + button.addEventListener('command', function() { + Services.obs.notifyObservers(null, `${id}-cmdPressed`, name); + }, true); + setting.appendChild(button); + } + else if (type === 'boolint') { + setting.setAttribute('on', on); + setting.setAttribute('off', off); + } + else if (type === 'menulist') { + let menulist = document.createElementNS(XUL_NS, 'menulist'); + let menupopup = document.createElementNS(XUL_NS, 'menupopup'); + for (let { value, label } of options) { + let menuitem = document.createElementNS(XUL_NS, 'menuitem'); + menuitem.setAttribute('value', value); + menuitem.setAttribute('label', label); + menupopup.appendChild(menuitem); + } + menulist.appendChild(menupopup); + setting.appendChild(menulist); + } + else if (type === 'radio') { + let radiogroup = document.createElementNS(XUL_NS, 'radiogroup'); + for (let { value, label } of options) { + let radio = document.createElementNS(XUL_NS, 'radio'); + radio.setAttribute('value', value); + radio.setAttribute('label', label); + radiogroup.appendChild(radio); + } + setting.appendChild(radiogroup); + } + + parent.appendChild(setting); + }); +} +exports.injectOptions = injectOptions; diff --git a/addon-sdk/source/lib/sdk/preferences/service.js b/addon-sdk/source/lib/sdk/preferences/service.js new file mode 100644 index 000000000..231cd8e14 --- /dev/null +++ b/addon-sdk/source/lib/sdk/preferences/service.js @@ -0,0 +1,137 @@ +/* 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" +}; + +// The minimum and maximum integers that can be set as preferences. +// The range of valid values is narrower than the range of valid JS values +// because the native preferences code treats integers as NSPR PRInt32s, +// which are 32-bit signed integers on all platforms. +const MAX_INT = 0x7FFFFFFF; +const MIN_INT = -0x80000000; + +const {Cc,Ci,Cr} = require("chrome"); + +const prefService = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); +const prefSvc = prefService.getBranch(null); +const defaultBranch = prefService.getDefaultBranch(null); + +const { Preferences } = require("resource://gre/modules/Preferences.jsm"); +const prefs = new Preferences({}); + +const branchKeys = branchName => + keys(branchName).map($ => $.replace(branchName, "")); + +const Branch = function(branchName) { + return new Proxy(Branch.prototype, { + getOwnPropertyDescriptor(target, name, receiver) { + return { + configurable: true, + enumerable: true, + writable: false, + value: this.get(target, name, receiver) + }; + }, + ownKeys(target) { + return branchKeys(branchName); + }, + get(target, name, receiver) { + return get(`${branchName}${name}`); + }, + set(target, name, value, receiver) { + set(`${branchName}${name}`, value); + return true; + }, + 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) { + 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 "Number": + if (value % 1 != 0) + throw new Error("cannot store non-integer number: " + value); + } + + prefs.set(name, value); +} +exports.set = set; + +const has = prefs.has.bind(prefs) +exports.has = has; + +function keys(root) { + return prefSvc.getChildList(root); +} +exports.keys = keys; + +const isSet = prefs.isSet.bind(prefs); +exports.isSet = isSet; + +function reset(name) { + try { + prefSvc.clearUserPref(name); + } + catch (e) { + // The pref service throws NS_ERROR_UNEXPECTED when the caller tries + // to reset a pref that doesn't exist or is already set to its default + // value. This interface fails silently in those cases, so callers + // can unconditionally reset a pref without having to check if it needs + // resetting first or trap exceptions after the fact. It passes through + // other exceptions, however, so callers know about them, since we don't + // know what other exceptions might be thrown and what they might mean. + if (e.result != Cr.NS_ERROR_UNEXPECTED) { + throw e; + } + } +} +exports.reset = reset; + +function getLocalized(name, defaultValue) { + let value = null; + try { + value = prefSvc.getComplexValue(name, Ci.nsIPrefLocalizedString).data; + } + finally { + return value || defaultValue; + } +} +exports.getLocalized = getLocalized; + +function setLocalized(name, value) { + // We can't use `prefs.set` here as we have to use `getDefaultBranch` + // (instead of `getBranch`) in order to have `mIsDefault` set to true, here: + // http://mxr.mozilla.org/mozilla-central/source/modules/libpref/src/nsPrefBranch.cpp#233 + // Otherwise, we do not enter into this expected condition: + // http://mxr.mozilla.org/mozilla-central/source/modules/libpref/src/nsPrefBranch.cpp#244 + defaultBranch.setCharPref(name, value); +} +exports.setLocalized = setLocalized; + +exports.Branch = Branch; + diff --git a/addon-sdk/source/lib/sdk/preferences/utils.js b/addon-sdk/source/lib/sdk/preferences/utils.js new file mode 100644 index 000000000..1d5769c37 --- /dev/null +++ b/addon-sdk/source/lib/sdk/preferences/utils.js @@ -0,0 +1,42 @@ +/* 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 { openTab, getBrowserForTab, getTabId } = require("sdk/tabs/utils"); +const { on, off } = require("sdk/system/events"); +const { getMostRecentBrowserWindow } = require('../window/utils'); + +// 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; + + // wait for the add-on's "addon-options-displayed" + on("addon-options-displayed", function onPrefDisplayed({ subject: doc, data }) { + if (data === id) { + off("addon-options-displayed", onPrefDisplayed); + 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); + }, true); +}); +exports.open = open; diff --git a/addon-sdk/source/lib/sdk/private-browsing.js b/addon-sdk/source/lib/sdk/private-browsing.js new file mode 100644 index 000000000..29ca16185 --- /dev/null +++ b/addon-sdk/source/lib/sdk/private-browsing.js @@ -0,0 +1,12 @@ +/* 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": "stable" +}; + +const { isPrivate } = require('./private-browsing/utils'); + +exports.isPrivate = isPrivate; diff --git a/addon-sdk/source/lib/sdk/private-browsing/utils.js b/addon-sdk/source/lib/sdk/private-browsing/utils.js new file mode 100644 index 000000000..8b012f0ce --- /dev/null +++ b/addon-sdk/source/lib/sdk/private-browsing/utils.js @@ -0,0 +1,54 @@ +/* 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, Cu } = require('chrome'); +const { is } = require('../system/xul-app'); +const { isWindowPrivate } = require('../window/utils'); +const { isPrivateBrowsingSupported } = require('../self'); +const { dispatcher } = require("../util/dispatcher"); + +var PrivateBrowsingUtils; + +// Private browsing is only supported in Fx +try { + PrivateBrowsingUtils = Cu.import('resource://gre/modules/PrivateBrowsingUtils.jsm', {}).PrivateBrowsingUtils; +} +catch (e) {} + +exports.isGlobalPBSupported = false; + +// checks that per-window private browsing is implemented +var isWindowPBSupported = exports.isWindowPBSupported = + !!PrivateBrowsingUtils && is('Firefox'); + +// checks that per-tab private browsing is implemented +var isTabPBSupported = exports.isTabPBSupported = + !!PrivateBrowsingUtils && is('Fennec'); + +function isPermanentPrivateBrowsing() { + return !!(PrivateBrowsingUtils && PrivateBrowsingUtils.permanentPrivateBrowsing); +} +exports.isPermanentPrivateBrowsing = isPermanentPrivateBrowsing; + +function ignoreWindow(window) { + return !isPrivateBrowsingSupported && isWindowPrivate(window); +} +exports.ignoreWindow = ignoreWindow; + +var getMode = function getMode(chromeWin) { + return (chromeWin !== undefined && isWindowPrivate(chromeWin)); +}; +exports.getMode = getMode; + +const isPrivate = dispatcher("isPrivate"); +isPrivate.when(isPermanentPrivateBrowsing, _ => true); +isPrivate.when(x => x instanceof Ci.nsIDOMWindow, isWindowPrivate); +isPrivate.when(x => Ci.nsIPrivateBrowsingChannel && x instanceof Ci.nsIPrivateBrowsingChannel, x => x.isChannelPrivate); +isPrivate.define(() => false); +exports.isPrivate = isPrivate; diff --git a/addon-sdk/source/lib/sdk/querystring.js b/addon-sdk/source/lib/sdk/querystring.js new file mode 100644 index 000000000..9982a00ab --- /dev/null +++ b/addon-sdk/source/lib/sdk/querystring.js @@ -0,0 +1,121 @@ +/* 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" +}; + +var unescape = decodeURIComponent; +exports.unescape = unescape; + +// encodes a string safely for application/x-www-form-urlencoded +// adheres to RFC 3986 +// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURIComponent +function escape(query) { + return encodeURIComponent(query). + replace(/%20/g, '+'). + replace(/!/g, '%21'). + replace(/'/g, '%27'). + replace(/\(/g, '%28'). + replace(/\)/g, '%29'). + replace(/\*/g, '%2A'); +} +exports.escape = escape; + +// Converts an object of unordered key-vals to a string that can be passed +// as part of a request +function stringify(options, separator, assigner) { + separator = separator || '&'; + assigner = assigner || '='; + // Explicitly return null if we have null, and empty string, or empty object. + if (!options) + return ''; + + // If content is already a string, just return it as is. + if (typeof(options) == 'string') + return options; + + // At this point we have a k:v object. Iterate over it and encode each value. + // Arrays and nested objects will get encoded as needed. For example... + // + // { foo: [1, 2, { omg: 'bbq', 'all your base!': 'are belong to us' }], bar: 'baz' } + // + // will be encoded as + // + // foo[0]=1&foo[1]=2&foo[2][omg]=bbq&foo[2][all+your+base!]=are+belong+to+us&bar=baz + // + // Keys (including '[' and ']') and values will be encoded with + // `escape` before returning. + // + // Execution was inspired by jQuery, but some details have changed and numeric + // array keys are included (whereas they are not in jQuery). + + let encodedContent = []; + function add(key, val) { + encodedContent.push(escape(key) + assigner + escape(val)); + } + + function make(key, value) { + if (value && typeof(value) === 'object') + Object.keys(value).forEach(function(name) { + make(key + '[' + name + ']', value[name]); + }); + else + add(key, value); + } + + Object.keys(options).forEach(function(name) { make(name, options[name]); }); + return encodedContent.join(separator); + + //XXXzpao In theory, we can just use a FormData object on 1.9.3, but I had + // trouble getting that working. It would also be nice to stay + // backwards-compat as long as possible. Keeping this in for now... + // let formData = Cc['@mozilla.org/files/formdata;1']. + // createInstance(Ci.nsIDOMFormData); + // for ([k, v] in Iterator(content)) { + // formData.append(k, v); + // } + // return formData; +} +exports.stringify = stringify; + +// Exporting aliases that nodejs implements just for the sake of +// interoperability. +exports.encode = stringify; +exports.serialize = stringify; + +// Note: That `stringify` and `parse` aren't bijective as we use `stringify` +// as it was implement in request module, but implement `parse` to match nodejs +// behavior. +// TODO: Make `stringify` implement API as in nodejs and figure out backwards +// compatibility. +function parse(query, separator, assigner) { + separator = separator || '&'; + assigner = assigner || '='; + let result = {}; + + if (typeof query !== 'string' || query.length === 0) + return result; + + query.split(separator).forEach(function(chunk) { + let pair = chunk.split(assigner); + let key = unescape(pair[0]); + let value = unescape(pair.slice(1).join(assigner)); + + if (!(key in result)) + result[key] = value; + else if (Array.isArray(result[key])) + result[key].push(value); + else + result[key] = [result[key], value]; + }); + + return result; +}; +exports.parse = parse; +// Exporting aliases that nodejs implements just for the sake of +// interoperability. +exports.decode = parse; diff --git a/addon-sdk/source/lib/sdk/remote/child.js b/addon-sdk/source/lib/sdk/remote/child.js new file mode 100644 index 000000000..4ccfa661a --- /dev/null +++ b/addon-sdk/source/lib/sdk/remote/child.js @@ -0,0 +1,284 @@ +/* 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 { isChildLoader } = require('./core'); +if (!isChildLoader) + throw new Error("Cannot load sdk/remote/child in a main process loader."); + +const { Ci, Cc, Cu } = require('chrome'); +const runtime = require('../system/runtime'); +const { Class } = require('../core/heritage'); +const { Namespace } = require('../core/namespace'); +const { omit } = require('../util/object'); +const { when } = require('../system/unload'); +const { EventTarget } = require('../event/target'); +const { emit } = require('../event/core'); +const { Disposable } = require('../core/disposable'); +const { EventParent } = require('./utils'); +const { addListItem, removeListItem } = require('../util/list'); + +const loaderID = require('@loader/options').loaderID; + +const MAIN_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + +const mm = Cc['@mozilla.org/childprocessmessagemanager;1']. + getService(Ci.nsISyncMessageSender); + +const ns = Namespace(); + +const process = { + port: new EventTarget(), + get id() { + return runtime.processID; + }, + get isRemote() { + return runtime.processType != MAIN_PROCESS; + } +}; +exports.process = process; + +function definePort(obj, name) { + obj.port.emit = (event, ...args) => { + let manager = ns(obj).messageManager; + if (!manager) + return; + + manager.sendAsyncMessage(name, { loaderID, event, args }); + }; +} + +function messageReceived({ data, objects }) { + // Ignore messages from other loaders + if (data.loaderID != loaderID) + return; + + let keys = Object.keys(objects); + if (keys.length) { + // If any objects are CPOWs then ignore this message. We don't want child + // processes interracting with CPOWs + if (!keys.every(name => !Cu.isCrossProcessWrapper(objects[name]))) + return; + + data.args.push(objects); + } + + emit(this.port, data.event, this, ...data.args); +} + +ns(process).messageManager = mm; +definePort(process, 'sdk/remote/process/message'); +let processMessageReceived = messageReceived.bind(process); +mm.addMessageListener('sdk/remote/process/message', processMessageReceived); + +when(() => { + mm.removeMessageListener('sdk/remote/process/message', processMessageReceived); + frames = null; +}); + +process.port.on('sdk/remote/require', (process, uri) => { + require(uri); +}); + +function listenerEquals(a, b) { + for (let prop of ["type", "callback", "isCapturing"]) { + if (a[prop] != b[prop]) + return false; + } + return true; +} + +function listenerFor(type, callback, isCapturing = false) { + return { + type, + callback, + isCapturing, + registeredCallback: undefined, + get args() { + return [ + this.type, + this.registeredCallback ? this.registeredCallback : this.callback, + this.isCapturing + ]; + } + }; +} + +function removeListenerFromArray(array, listener) { + let index = array.findIndex(l => listenerEquals(l, listener)); + if (index < 0) + return; + array.splice(index, 1); +} + +function getListenerFromArray(array, listener) { + return array.find(l => listenerEquals(l, listener)); +} + +function arrayContainsListener(array, listener) { + return !!getListenerFromArray(array, listener); +} + +function makeFrameEventListener(frame, callback) { + return callback.bind(frame); +} + +var FRAME_ID = 0; +var tabMap = new Map(); + +const Frame = Class({ + implements: [ Disposable ], + extends: EventTarget, + setup: function(contentFrame) { + // This ID should be unique for this loader across all processes + let priv = ns(this); + + priv.id = runtime.processID + ":" + FRAME_ID++; + + priv.contentFrame = contentFrame; + priv.messageManager = contentFrame; + priv.domListeners = []; + + tabMap.set(contentFrame.docShell, this); + + priv.messageReceived = messageReceived.bind(this); + priv.messageManager.addMessageListener('sdk/remote/frame/message', priv.messageReceived); + + this.port = new EventTarget(); + definePort(this, 'sdk/remote/frame/message'); + + priv.messageManager.sendAsyncMessage('sdk/remote/frame/attach', { + loaderID, + frameID: priv.id, + processID: runtime.processID + }); + + frames.attachItem(this); + }, + + dispose: function() { + let priv = ns(this); + + emit(this, 'detach', this); + + for (let listener of priv.domListeners) + priv.contentFrame.removeEventListener(...listener.args); + + priv.messageManager.removeMessageListener('sdk/remote/frame/message', priv.messageReceived); + tabMap.delete(priv.contentFrame.docShell); + priv.contentFrame = null; + }, + + get content() { + return ns(this).contentFrame.content; + }, + + get isTab() { + let docShell = ns(this).contentFrame.docShell; + if (process.isRemote) { + // We don't want to roundtrip to the main process to get this property. + // This hack relies on the host app having defined webBrowserChrome only + // in frames that are part of the tabs. Since only Firefox has remote + // processes right now and does this this works. + let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsITabChild); + return !!tabchild.webBrowserChrome; + } + else { + // This is running in the main process so we can break out to the browser + // And check we can find a tab for the browser element directly. + let browser = docShell.chromeEventHandler; + let tab = require('../tabs/utils').getTabForBrowser(browser); + return !!tab; + } + }, + + addEventListener: function(...args) { + let priv = ns(this); + + let listener = listenerFor(...args); + if (arrayContainsListener(priv.domListeners, listener)) + return; + + listener.registeredCallback = makeFrameEventListener(this, listener.callback); + + priv.domListeners.push(listener); + priv.contentFrame.addEventListener(...listener.args); + }, + + removeEventListener: function(...args) { + let priv = ns(this); + + let listener = getListenerFromArray(priv.domListeners, listenerFor(...args)); + if (!listener) + return; + + removeListenerFromArray(priv.domListeners, listener); + priv.contentFrame.removeEventListener(...listener.args); + } +}); + +const FrameList = Class({ + implements: [ EventParent, Disposable ], + extends: EventTarget, + setup: function() { + EventParent.prototype.initialize.call(this); + + this.port = new EventTarget(); + ns(this).domListeners = []; + + this.on('attach', frame => { + for (let listener of ns(this).domListeners) + frame.addEventListener(...listener.args); + }); + }, + + dispose: function() { + // The only case where we get destroyed is when the loader is unloaded in + // which case each frame will clean up its own event listeners. + ns(this).domListeners = null; + }, + + getFrameForWindow: function(window) { + let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + + return tabMap.get(docShell) || null; + }, + + addEventListener: function(...args) { + let listener = listenerFor(...args); + if (arrayContainsListener(ns(this).domListeners, listener)) + return; + + ns(this).domListeners.push(listener); + for (let frame of this) + frame.addEventListener(...listener.args); + }, + + removeEventListener: function(...args) { + let listener = listenerFor(...args); + if (!arrayContainsListener(ns(this).domListeners, listener)) + return; + + removeListenerFromArray(ns(this).domListeners, listener); + for (let frame of this) + frame.removeEventListener(...listener.args); + } +}); +var frames = exports.frames = new FrameList(); + +function registerContentFrame(contentFrame) { + let frame = new Frame(contentFrame); +} +exports.registerContentFrame = registerContentFrame; + +function unregisterContentFrame(contentFrame) { + let frame = tabMap.get(contentFrame.docShell); + if (!frame) + return; + + frame.destroy(); +} +exports.unregisterContentFrame = unregisterContentFrame; diff --git a/addon-sdk/source/lib/sdk/remote/core.js b/addon-sdk/source/lib/sdk/remote/core.js new file mode 100644 index 000000000..78bb673fd --- /dev/null +++ b/addon-sdk/source/lib/sdk/remote/core.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 options = require("@loader/options"); + +exports.isChildLoader = options.childLoader; diff --git a/addon-sdk/source/lib/sdk/remote/parent.js b/addon-sdk/source/lib/sdk/remote/parent.js new file mode 100644 index 000000000..f110fe3f6 --- /dev/null +++ b/addon-sdk/source/lib/sdk/remote/parent.js @@ -0,0 +1,338 @@ +/* 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 { isChildLoader } = require('./core'); +if (isChildLoader) + throw new Error("Cannot load sdk/remote/parent in a child loader."); + +const { Cu, Ci, Cc } = require('chrome'); +const runtime = require('../system/runtime'); + +const MAIN_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + +if (runtime.processType != MAIN_PROCESS) { + throw new Error('Cannot use sdk/remote/parent in a child process.'); +} + +const { Class } = require('../core/heritage'); +const { Namespace } = require('../core/namespace'); +const { Disposable } = require('../core/disposable'); +const { omit } = require('../util/object'); +const { when } = require('../system/unload'); +const { EventTarget } = require('../event/target'); +const { emit } = require('../event/core'); +const system = require('../system/events'); +const { EventParent } = require('./utils'); +const options = require('@loader/options'); +const loaderModule = require('toolkit/loader'); +const { getTabForBrowser } = require('../tabs/utils'); + +const appInfo = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULRuntime); + +exports.useRemoteProcesses = appInfo.browserTabsRemoteAutostart; + +// Chose the right function for resolving relative a module id +var moduleResolve; +if (options.isNative) { + moduleResolve = (id, requirer) => loaderModule.nodeResolve(id, requirer, { rootURI: options.rootURI }); +} +else { + moduleResolve = loaderModule.resolve; +} +// Build the sorted path mapping structure that resolveURI requires +var pathMapping = Object.keys(options.paths) + .sort((a, b) => b.length - a.length) + .map(p => [p, options.paths[p]]); + +// Load the scripts in the child processes +var { getNewLoaderID } = require('../../framescript/FrameScriptManager.jsm'); +var PATH = options.paths['']; + +const childOptions = omit(options, ['modules', 'globals', 'resolve', 'load']); +childOptions.modules = {}; +// @l10n/data is just JSON data and can be safely sent across to the child loader +try { + childOptions.modules["@l10n/data"] = require("@l10n/data"); +} +catch (e) { + // There may be no l10n data +} +const loaderID = getNewLoaderID(); +childOptions.loaderID = loaderID; +childOptions.childLoader = true; + +const ppmm = Cc['@mozilla.org/parentprocessmessagemanager;1']. + getService(Ci.nsIMessageBroadcaster); +const gmm = Cc['@mozilla.org/globalmessagemanager;1']. + getService(Ci.nsIMessageBroadcaster); + +const ns = Namespace(); + +var processMap = new Map(); + +function definePort(obj, name) { + obj.port.emitCPOW = (event, args, cpows = {}) => { + let manager = ns(obj).messageManager; + if (!manager) + return; + + let method = manager instanceof Ci.nsIMessageBroadcaster ? + "broadcastAsyncMessage" : "sendAsyncMessage"; + + manager[method](name, { loaderID, event, args }, cpows); + }; + + obj.port.emit = (event, ...args) => obj.port.emitCPOW(event, args); +} + +function messageReceived({ target, data }) { + // Ignore messages from other loaders + if (data.loaderID != loaderID) + return; + + emit(this.port, data.event, this, ...data.args); +} + +// Process represents a gecko process that can load webpages. Each process +// contains a number of Frames. This class is used to send and receive messages +// from a single process. +const Process = Class({ + implements: [ Disposable ], + extends: EventTarget, + setup: function(id, messageManager, isRemote) { + ns(this).id = id; + ns(this).isRemote = isRemote; + ns(this).messageManager = messageManager; + ns(this).messageReceived = messageReceived.bind(this); + this.destroy = this.destroy.bind(this); + ns(this).messageManager.addMessageListener('sdk/remote/process/message', ns(this).messageReceived); + ns(this).messageManager.addMessageListener('child-process-shutdown', this.destroy); + + this.port = new EventTarget(); + definePort(this, 'sdk/remote/process/message'); + + // Load any remote modules + for (let module of remoteModules.values()) + this.port.emit('sdk/remote/require', module); + + processMap.set(ns(this).id, this); + processes.attachItem(this); + }, + + dispose: function() { + emit(this, 'detach', this); + processMap.delete(ns(this).id); + ns(this).messageManager.removeMessageListener('sdk/remote/process/message', ns(this).messageReceived); + ns(this).messageManager.removeMessageListener('child-process-shutdown', this.destroy); + ns(this).messageManager = null; + }, + + // Returns true if this process is a child process + get isRemote() { + return ns(this).isRemote; + } +}); + +// Processes gives an API for enumerating an sending and receiving messages from +// all processes as well as detecting when a new process starts. +const Processes = Class({ + implements: [ EventParent ], + extends: EventTarget, + initialize: function() { + EventParent.prototype.initialize.call(this); + ns(this).messageManager = ppmm; + + this.port = new EventTarget(); + definePort(this, 'sdk/remote/process/message'); + }, + + getById: function(id) { + return processMap.get(id); + } +}); +var processes = exports.processes = new Processes(); + +var frameMap = new Map(); + +function setFrameProcess(frame, process) { + ns(frame).process = process; + frames.attachItem(frame); +} + +// Frames display webpages in a process. In the main process every Frame is +// linked with a or + + diff --git a/addon-sdk/source/test/fixtures/test-iframe.js b/addon-sdk/source/test/fixtures/test-iframe.js new file mode 100644 index 000000000..bbfadc4ff --- /dev/null +++ b/addon-sdk/source/test/fixtures/test-iframe.js @@ -0,0 +1,16 @@ +/* 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/. */ + +var count = 0; + +setTimeout(function() { + window.addEventListener("message", function(msg) { + if (++count > 1) { + self.postMessage(msg.data); + } + else msg.source.postMessage(msg.data, '*'); + }); + + document.getElementById('inner').src = iframePath; +}, 0); diff --git a/addon-sdk/source/test/fixtures/test-message-manager.js b/addon-sdk/source/test/fixtures/test-message-manager.js new file mode 100644 index 000000000..d647bd8fd --- /dev/null +++ b/addon-sdk/source/test/fixtures/test-message-manager.js @@ -0,0 +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/. */ + +const TEST_VALUE = 11; + diff --git a/addon-sdk/source/test/fixtures/test-net-url.txt b/addon-sdk/source/test/fixtures/test-net-url.txt new file mode 100644 index 000000000..9f8166e61 --- /dev/null +++ b/addon-sdk/source/test/fixtures/test-net-url.txt @@ -0,0 +1 @@ +Hello, ゼロ! \ No newline at end of file diff --git a/addon-sdk/source/test/fixtures/test-page-mod.html b/addon-sdk/source/test/fixtures/test-page-mod.html new file mode 100644 index 000000000..901abefc4 --- /dev/null +++ b/addon-sdk/source/test/fixtures/test-page-mod.html @@ -0,0 +1,12 @@ + + + + + Page Mod test + + +

Lorem ipsum dolor sit amet.

+ + diff --git a/addon-sdk/source/test/fixtures/test-sidebar-addon-global.html b/addon-sdk/source/test/fixtures/test-sidebar-addon-global.html new file mode 100644 index 000000000..4552e3d78 --- /dev/null +++ b/addon-sdk/source/test/fixtures/test-sidebar-addon-global.html @@ -0,0 +1,15 @@ + + +SIDEBAR TEST diff --git a/addon-sdk/source/test/fixtures/test-trusted-document.html b/addon-sdk/source/test/fixtures/test-trusted-document.html new file mode 100644 index 000000000..c31e055cb --- /dev/null +++ b/addon-sdk/source/test/fixtures/test-trusted-document.html @@ -0,0 +1,20 @@ + + + + + + Worker test + + +

Lorem ipsum dolor sit amet.

+ + + diff --git a/addon-sdk/source/test/fixtures/test.html b/addon-sdk/source/test/fixtures/test.html new file mode 100644 index 000000000..70b5e31e5 --- /dev/null +++ b/addon-sdk/source/test/fixtures/test.html @@ -0,0 +1,25 @@ + + + + + + foo + + +

bar

+ + + diff --git a/addon-sdk/source/test/fixtures/testLocalXhr.json b/addon-sdk/source/test/fixtures/testLocalXhr.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/addon-sdk/source/test/fixtures/testLocalXhr.json @@ -0,0 +1 @@ +{} diff --git a/addon-sdk/source/test/framescript-manager/frame-script.js b/addon-sdk/source/test/framescript-manager/frame-script.js new file mode 100644 index 000000000..de9bb8385 --- /dev/null +++ b/addon-sdk/source/test/framescript-manager/frame-script.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"; + +const {onPing} = require("./pong"); + +exports.onPing = onPing; + +exports.onInit = (context) => { + context.sendAsyncMessage("framescript-manager/ready", {state: "ready"}); + context.addMessageListener("framescript-manager/ping", exports.onPing); +}; diff --git a/addon-sdk/source/test/framescript-manager/pong.js b/addon-sdk/source/test/framescript-manager/pong.js new file mode 100644 index 000000000..b7fb7e077 --- /dev/null +++ b/addon-sdk/source/test/framescript-manager/pong.js @@ -0,0 +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"; + +exports.onPing = message => + message.target.sendAsyncMessage("framescript-manager/pong", message.data); diff --git a/addon-sdk/source/test/framescript-util/frame-script.js b/addon-sdk/source/test/framescript-util/frame-script.js new file mode 100644 index 000000000..2a4a2284c --- /dev/null +++ b/addon-sdk/source/test/framescript-util/frame-script.js @@ -0,0 +1,21 @@ +/* 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 { windowToMessageManager, nodeToMessageManager } = require("framescript/util"); + + +const onInit = (context) => { + context.addMessageListener("framescript-util/window/request", _ => { + windowToMessageManager(context.content.window).sendAsyncMessage( + "framescript-util/window/response", {window: true}); + }); + + context.addMessageListener("framescript-util/node/request", message => { + const node = context.content.document.querySelector(message.data); + nodeToMessageManager(node).sendAsyncMessage( + "framescript-util/node/response", {node: true}); + }); +}; +exports.onInit = onInit; diff --git a/addon-sdk/source/test/jetpack-package.ini b/addon-sdk/source/test/jetpack-package.ini new file mode 100644 index 000000000..fa93b9c2f --- /dev/null +++ b/addon-sdk/source/test/jetpack-package.ini @@ -0,0 +1,179 @@ +[DEFAULT] +support-files = + buffers/** + commonjs-test-adapter/** + context-menu/** + event/** + fixtures.js + fixtures/** + fixtures/native-addon-test.xpi + fixtures/native-overrides-test.xpi + framescript-manager/** + framescript-util/** + lib/** + loader/** + modules/** + page-mod/** + path/** + private-browsing/** + querystring/** + sidebar/** + tabs/** + test-context-menu.html + traits/** + util.js + windows/** + zip/** +generated-files = + fixtures/native-addon-test.xpi + fixtures/native-overrides-test.xpi + +[test-addon-bootstrap.js] +[test-addon-extras.js] +[test-addon-installer.js] +[test-addon-window.js] +[test-api-utils.js] +[test-array.js] +[test-base64.js] +[test-bootstrap.js] +[test-browser-events.js] +[test-buffer.js] +[test-byte-streams.js] +[test-child_process.js] +[test-chrome.js] +[test-clipboard.js] +subsuite = clipboard +[test-collection.js] +[test-commonjs-test-adapter.js] +[test-content-events.js] +[test-content-script.js] +[test-content-sync-worker.js] +[test-content-worker.js] +[test-context-menu.js] +[test-context-menu@2.js] +[test-cuddlefish.js] +# Cuddlefish loader is unsupported +skip-if = true +[test-deprecate.js] +[test-dev-panel.js] +[test-diffpatcher.js] +[test-dispatcher.js] +[test-disposable.js] +[test-dom.js] +[test-environment.js] +[test-event-core.js] +[test-event-dom.js] +[test-event-target.js] +[test-event-utils.js] +[test-file.js] +[test-frame-utils.js] +[test-framescript-manager.js] +[test-framescript-util.js] +[test-fs.js] +[test-functional.js] +[test-globals.js] +[test-heritage.js] +[test-hidden-frame.js] +[test-host-events.js] +[test-hotkeys.js] +[test-httpd.js] +[test-indexed-db.js] +[test-jetpack-id.js] +[test-keyboard-observer.js] +[test-keyboard-utils.js] +[test-l10n-locale.js] +[test-l10n-plural-rules.js] +[test-lang-type.js] +[test-libxul.js] +[test-list.js] +[test-loader.js] +[test-match-pattern.js] +[test-method.js] +[test-module.js] +[test-modules.js] +[test-mozilla-toolkit-versioning.js] +[test-mpl2-license-header.js] +skip-if = true +[test-namespace.js] +[test-native-loader.js] +[test-native-options.js] +[test-net-url.js] +[test-node-os.js] +[test-notifications.js] +[test-object.js] +[test-observers.js] +[test-page-mod-debug.js] +[test-page-mod.js] +[test-page-worker.js] +[test-panel.js] +[test-passwords-utils.js] +[test-passwords.js] +[test-path.js] +[test-plain-text-console.js] +[test-preferences-service.js] +[test-preferences-target.js] +[test-private-browsing.js] +[test-promise.js] +[test-querystring.js] +[test-reference.js] +[test-request.js] +[test-require.js] +[test-rules.js] +[test-sandbox.js] +[test-selection.js] +[test-self.js] +[test-sequence.js] +[test-set-exports.js] +[test-shared-require.js] +[test-simple-prefs.js] +[test-simple-storage.js] +[test-system-events.js] +[test-system-input-output.js] +[test-system-runtime.js] +[test-system-startup.js] +[test-system.js] +[test-tab-events.js] +[test-tab-observer.js] +[test-tab-utils.js] +[test-tab.js] +[test-tabs-common.js] +[test-tabs.js] +[test-test-addon-file.js] +[test-test-assert.js] +[test-test-loader.js] +[test-test-memory.js] +[test-test-utils-async.js] +[test-test-utils-generator.js] +[test-test-utils-sync.js] +[test-test-utils.js] +[test-text-streams.js] +[test-timer.js] +[test-traceback.js] +[test-ui-action-button.js] +skip-if = debug || asan # Bug 1208727 +[test-ui-frame.js] +[test-ui-id.js] +[test-ui-sidebar-private-browsing.js] +[test-ui-sidebar.js] +[test-ui-toggle-button.js] +[test-ui-toolbar.js] +[test-unit-test-finder.js] +[test-unit-test.js] +[test-unload.js] +[test-unsupported-skip.js] +# Bug 1037235 +skip-if = true +[test-uri-resource.js] +[test-url.js] +[test-uuid.js] +[test-weak-set.js] +[test-window-events.js] +[test-window-observer.js] +[test-window-utils-private-browsing.js] +[test-window-utils.js] +[test-window-utils2.js] +[test-windows-common.js] +[test-windows.js] +[test-xhr.js] +[test-xpcom.js] +[test-xul-app.js] diff --git a/addon-sdk/source/test/leak/jetpack-package.ini b/addon-sdk/source/test/leak/jetpack-package.ini new file mode 100644 index 000000000..0632bdc87 --- /dev/null +++ b/addon-sdk/source/test/leak/jetpack-package.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + leak-utils.js + +[test-leak-window-events.js] +[test-leak-event-dom-closed-window.js] +[test-leak-tab-events.js] diff --git a/addon-sdk/source/test/leak/leak-utils.js b/addon-sdk/source/test/leak/leak-utils.js new file mode 100644 index 000000000..e01255ec8 --- /dev/null +++ b/addon-sdk/source/test/leak/leak-utils.js @@ -0,0 +1,80 @@ +/* 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, Ci } = require("chrome"); +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { SelfSupportBackend } = Cu.import("resource:///modules/SelfSupportBackend.jsm", {}); +const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports; + +// Adapted from the SpecialPowers.exactGC() code. We don't have a +// window to operate on so we cannot use the exact same logic. We +// use 6 GC iterations here as that is what is needed to clean up +// the windows we have tested with. +function gc() { + return new Promise(resolve => { + Cu.forceGC(); + Cu.forceCC(); + let count = 0; + function genGCCallback() { + Cu.forceCC(); + return function() { + if (++count < 5) { + Cu.schedulePreciseGC(genGCCallback()); + } else { + resolve(); + } + } + } + + Cu.schedulePreciseGC(genGCCallback()); + }); +} + +// Execute the given test function and verify that we did not leak windows +// in the process. The test function must return a promise or be a generator. +// If the promise is resolved, or generator completes, with an sdk loader +// object then it will be unloaded after the memory measurements. +exports.asyncWindowLeakTest = function*(assert, asyncTestFunc) { + + // SelfSupportBackend periodically tries to open windows. This can + // mess up our window leak detection below, so turn it off. + SelfSupportBackend.uninit(); + + // Wait for the browser to finish loading. + yield Startup.onceInitialized; + + // Track windows that are opened in an array of weak references. + let weakWindows = []; + function windowObserver(subject, topic) { + let supportsWeak = subject.QueryInterface(Ci.nsISupportsWeakReference); + if (supportsWeak) { + weakWindows.push(Cu.getWeakReference(supportsWeak)); + } + } + Services.obs.addObserver(windowObserver, "domwindowopened", false); + + // Execute the body of the test. + let testLoader = yield asyncTestFunc(assert); + + // Stop tracking new windows and attempt to GC any resources allocated + // by the test body. + Services.obs.removeObserver(windowObserver, "domwindowopened", false); + yield gc(); + + // Check to see if any of the windows we saw survived the GC. We consider + // these leaks. + assert.ok(weakWindows.length > 0, "should see at least one new window"); + for (let i = 0; i < weakWindows.length; ++i) { + assert.equal(weakWindows[i].get(), null, "window " + i + " should be GC'd"); + } + + // Finally, unload the test body's loader if it provided one. We do this + // after our leak detection to avoid free'ing things on unload. Users + // don't tend to unload their addons very often, so we want to find leaks + // that happen while addons are in use. + if (testLoader) { + testLoader.unload(); + } +} diff --git a/addon-sdk/source/test/leak/test-leak-event-dom-closed-window.js b/addon-sdk/source/test/leak/test-leak-event-dom-closed-window.js new file mode 100644 index 000000000..c398462ab --- /dev/null +++ b/addon-sdk/source/test/leak/test-leak-event-dom-closed-window.js @@ -0,0 +1,29 @@ +/* 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 { asyncWindowLeakTest } = require("./leak-utils"); +const { Loader } = require('sdk/test/loader'); +const openWindow = require("sdk/window/utils").open; + +exports["test sdk/event/dom does not leak when attached to closed window"] = function*(assert) { + yield asyncWindowLeakTest(assert, _ => { + return new Promise(resolve => { + let loader = Loader(module); + let { open } = loader.require('sdk/event/dom'); + let w = openWindow(); + w.addEventListener("DOMWindowClose", function windowClosed(evt) { + w.removeEventListener("DOMWindowClose", windowClosed); + // The sdk/event/dom module tries to clean itself up when DOMWindowClose + // is fired. Verify that it doesn't leak if its attached to an + // already closed window either. (See bug 1268898.) + open(w.document, "TestEvent1"); + resolve(loader); + }); + w.close(); + }); + }); +} + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/leak/test-leak-tab-events.js b/addon-sdk/source/test/leak/test-leak-tab-events.js new file mode 100644 index 000000000..4266c04fc --- /dev/null +++ b/addon-sdk/source/test/leak/test-leak-tab-events.js @@ -0,0 +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'; + +const { asyncWindowLeakTest } = require("./leak-utils"); +const { Loader } = require('sdk/test/loader'); +const openWindow = require("sdk/window/utils").open; + +exports["test sdk/tab/events does not leak new window"] = function*(assert) { + yield asyncWindowLeakTest(assert, _ => { + return new Promise(resolve => { + let loader = Loader(module); + let { events } = loader.require('sdk/tab/events'); + let w = openWindow(); + w.addEventListener("load", function windowLoaded(evt) { + w.removeEventListener("load", windowLoaded); + w.addEventListener("DOMWindowClose", function windowClosed(evt) { + w.removeEventListener("DOMWindowClose", windowClosed); + resolve(loader); + }); + w.close(); + }); + }); + }); +} + +exports["test sdk/tab/events does not leak when attached to existing window"] = function*(assert) { + yield asyncWindowLeakTest(assert, _ => { + return new Promise(resolve => { + let loader = Loader(module); + let w = openWindow(); + w.addEventListener("load", function windowLoaded(evt) { + w.removeEventListener("load", windowLoaded); + let { events } = loader.require('sdk/tab/events'); + w.addEventListener("DOMWindowClose", function windowClosed(evt) { + w.removeEventListener("DOMWindowClose", windowClosed); + resolve(loader); + }); + w.close(); + }); + }); + }); +} + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/leak/test-leak-window-events.js b/addon-sdk/source/test/leak/test-leak-window-events.js new file mode 100644 index 000000000..ceb20f475 --- /dev/null +++ b/addon-sdk/source/test/leak/test-leak-window-events.js @@ -0,0 +1,65 @@ +/* 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"; + +// Opening new windows in Fennec causes issues +module.metadata = { + engines: { + 'Firefox': '*' + } +}; + +const { asyncWindowLeakTest } = require("./leak-utils.js"); +const { Loader } = require("sdk/test/loader"); +const { open } = require("sdk/window/utils"); + +exports["test window/events for leaks"] = function*(assert) { + yield asyncWindowLeakTest(assert, _ => { + return new Promise((resolve, reject) => { + let loader = Loader(module); + let { events } = loader.require("sdk/window/events"); + let { on, off } = loader.require("sdk/event/core"); + + on(events, "data", function handler(e) { + try { + if (e.type === "load") { + e.target.close(); + } + else if (e.type === "close") { + off(events, "data", handler); + + // Let asyncWindowLeakTest call loader.unload() after the + // leak check. + resolve(loader); + } + } catch (e) { + reject(e); + } + }); + + // Open a window. This will trigger our data events. + open(); + }); + }); +}; + +exports["test window/events for leaks with existing window"] = function*(assert) { + yield asyncWindowLeakTest(assert, _ => { + return new Promise((resolve, reject) => { + let loader = Loader(module); + let w = open(); + w.addEventListener("load", function windowLoaded(evt) { + w.removeEventListener("load", windowLoaded); + let { events } = loader.require("sdk/window/events"); + w.addEventListener("DOMWindowClose", function windowClosed(evt) { + w.removeEventListener("DOMWindowClose", windowClosed); + resolve(loader); + }); + w.close(); + }); + }); + }); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/lib/httpd.js b/addon-sdk/source/test/lib/httpd.js new file mode 100644 index 000000000..e46ca96a0 --- /dev/null +++ b/addon-sdk/source/test/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, 0o444, + 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, 0o444, + 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, 0o444, + 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/loader/b2g.js b/addon-sdk/source/test/loader/b2g.js new file mode 100644 index 000000000..2982a0202 --- /dev/null +++ b/addon-sdk/source/test/loader/b2g.js @@ -0,0 +1,41 @@ +/* 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, Ci, Cu} = require("chrome"); +const {readURISync} = require("sdk/net/url"); + +const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]. + createInstance(Ci.nsIPrincipal); + + +const FakeCu = function() { + const sandbox = Cu.Sandbox(systemPrincipal, {wantXrays: false}); + sandbox.toString = function() { + return "[object BackstagePass]"; + } + this.sandbox = sandbox; +} +FakeCu.prototype = { + ["import"](url, scope) { + const {sandbox} = this; + sandbox.__URI__ = url; + const target = Cu.createObjectIn(sandbox); + target.toString = sandbox.toString; + Cu.evalInSandbox(`(function(){` + readURISync(url) + `\n})`, + sandbox, "1.8", url).call(target); + // Borrowed from mozJSComponentLoader.cpp to match errors closer. + // https://github.com/mozilla/gecko-dev/blob/f6ca65e8672433b2ce1a0e7c31f72717930b5e27/js/xpconnect/loader/mozJSComponentLoader.cpp#L1205-L1208 + if (!Array.isArray(target.EXPORTED_SYMBOLS)) { + throw Error("EXPORTED_SYMBOLS is not an array."); + } + + for (let key of target.EXPORTED_SYMBOLS) { + scope[key] = target[key]; + } + + return target; + } +}; +exports.FakeCu = FakeCu; diff --git a/addon-sdk/source/test/loader/fixture.js b/addon-sdk/source/test/loader/fixture.js new file mode 100644 index 000000000..ebf91abba --- /dev/null +++ b/addon-sdk/source/test/loader/fixture.js @@ -0,0 +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/. */ + +exports.foo = foo; +exports.bar = 2; +print('testing'); diff --git a/addon-sdk/source/test/loader/user-global.js b/addon-sdk/source/test/loader/user-global.js new file mode 100644 index 000000000..34b233f42 --- /dev/null +++ b/addon-sdk/source/test/loader/user-global.js @@ -0,0 +1,11 @@ +/* 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 module to check presense of user defined globals. +// Related to bug 827792. + +exports.getCom = function() { + return com; +}; diff --git a/addon-sdk/source/test/modules/add.js b/addon-sdk/source/test/modules/add.js new file mode 100644 index 000000000..54729340d --- /dev/null +++ b/addon-sdk/source/test/modules/add.js @@ -0,0 +1,9 @@ +/* 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/. */ + +define('modules/add', function () { + return function (a, b) { + return a + b; + }; +}); diff --git a/addon-sdk/source/test/modules/async1.js b/addon-sdk/source/test/modules/async1.js new file mode 100644 index 000000000..b7b60fdc2 --- /dev/null +++ b/addon-sdk/source/test/modules/async1.js @@ -0,0 +1,14 @@ +/* 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/. */ + +define(['./traditional2', './async2'], function () { + var traditional2 = require('./traditional2'); + return { + name: 'async1', + traditional1Name: traditional2.traditional1Name, + traditional2Name: traditional2.name, + async2Name: require('./async2').name, + async2Traditional2Name: require('./async2').traditional2Name + }; +}); diff --git a/addon-sdk/source/test/modules/async2.js b/addon-sdk/source/test/modules/async2.js new file mode 100644 index 000000000..802fb2504 --- /dev/null +++ b/addon-sdk/source/test/modules/async2.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/. */ + +define(['./traditional2', 'exports'], function (traditional2, exports) { + exports.name = 'async2'; + exports.traditional2Name = traditional2.name; +}); diff --git a/addon-sdk/source/test/modules/badExportAndReturn.js b/addon-sdk/source/test/modules/badExportAndReturn.js new file mode 100644 index 000000000..35a5359f6 --- /dev/null +++ b/addon-sdk/source/test/modules/badExportAndReturn.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/. */ + +// This is a bad module, it asks for exports but also returns a value from +// the define defintion function. +define(['exports'], function (exports) { + return 'badExportAndReturn'; +}); + diff --git a/addon-sdk/source/test/modules/badFirst.js b/addon-sdk/source/test/modules/badFirst.js new file mode 100644 index 000000000..fdb03bd4d --- /dev/null +++ b/addon-sdk/source/test/modules/badFirst.js @@ -0,0 +1,9 @@ +/* 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/. */ + +define(['./badSecond'], function (badSecond) { + return { + name: 'badFirst' + }; +}); diff --git a/addon-sdk/source/test/modules/badSecond.js b/addon-sdk/source/test/modules/badSecond.js new file mode 100644 index 000000000..8321a8542 --- /dev/null +++ b/addon-sdk/source/test/modules/badSecond.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/. */ + +var first = require('./badFirst'); + +exports.name = 'badSecond'; +exports.badFirstName = first.name; diff --git a/addon-sdk/source/test/modules/blue.js b/addon-sdk/source/test/modules/blue.js new file mode 100644 index 000000000..14ab14933 --- /dev/null +++ b/addon-sdk/source/test/modules/blue.js @@ -0,0 +1,9 @@ +/* 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/. */ + +define(function () { + return { + name: 'blue' + }; +}); diff --git a/addon-sdk/source/test/modules/castor.js b/addon-sdk/source/test/modules/castor.js new file mode 100644 index 000000000..9209623b5 --- /dev/null +++ b/addon-sdk/source/test/modules/castor.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/. */ + +define(['exports', './pollux'], function(exports, pollux) { + exports.name = 'castor'; + exports.getPolluxName = function () { + return pollux.name; + }; +}); diff --git a/addon-sdk/source/test/modules/cheetah.js b/addon-sdk/source/test/modules/cheetah.js new file mode 100644 index 000000000..37d12bfc1 --- /dev/null +++ b/addon-sdk/source/test/modules/cheetah.js @@ -0,0 +1,9 @@ +/* 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/. */ + +define(function () { + return function () { + return 'cheetah'; + }; +}); diff --git a/addon-sdk/source/test/modules/color.js b/addon-sdk/source/test/modules/color.js new file mode 100644 index 000000000..0d946bd99 --- /dev/null +++ b/addon-sdk/source/test/modules/color.js @@ -0,0 +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/. */ + +define({ + type: 'color' +}); diff --git a/addon-sdk/source/test/modules/dupe.js b/addon-sdk/source/test/modules/dupe.js new file mode 100644 index 000000000..f87fa555a --- /dev/null +++ b/addon-sdk/source/test/modules/dupe.js @@ -0,0 +1,15 @@ +/* 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/. */ + +define({ + name: 'dupe' +}); + +// This is wrong and should not be allowed. Only one call to +// define per file. +define([], function () { + return { + name: 'dupe2' + }; +}); diff --git a/addon-sdk/source/test/modules/dupeNested.js b/addon-sdk/source/test/modules/dupeNested.js new file mode 100644 index 000000000..703948ca7 --- /dev/null +++ b/addon-sdk/source/test/modules/dupeNested.js @@ -0,0 +1,15 @@ +/* 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/. */ + + +define(function () { + // This is wrong and should not be allowed. + define('dupeNested2', { + name: 'dupeNested2' + }); + + return { + name: 'dupeNested' + }; +}); diff --git a/addon-sdk/source/test/modules/dupeSetExports.js b/addon-sdk/source/test/modules/dupeSetExports.js new file mode 100644 index 000000000..12a1be22b --- /dev/null +++ b/addon-sdk/source/test/modules/dupeSetExports.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/. */ + +define({name: "dupeSetExports"}); + +// so this should cause a failure +module.setExports("no no no"); diff --git a/addon-sdk/source/test/modules/exportsEquals.js b/addon-sdk/source/test/modules/exportsEquals.js new file mode 100644 index 000000000..e176316f4 --- /dev/null +++ b/addon-sdk/source/test/modules/exportsEquals.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/. */ + +module.exports = 4; diff --git a/addon-sdk/source/test/modules/green.js b/addon-sdk/source/test/modules/green.js new file mode 100644 index 000000000..ce2d134b9 --- /dev/null +++ b/addon-sdk/source/test/modules/green.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/. */ + +define('modules/green', ['./color'], function (color) { + return { + name: 'green', + parentType: color.type + }; +}); diff --git a/addon-sdk/source/test/modules/lion.js b/addon-sdk/source/test/modules/lion.js new file mode 100644 index 000000000..9c3ac3bfe --- /dev/null +++ b/addon-sdk/source/test/modules/lion.js @@ -0,0 +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/. */ + +define(function(require) { + return 'lion'; +}); diff --git a/addon-sdk/source/test/modules/orange.js b/addon-sdk/source/test/modules/orange.js new file mode 100644 index 000000000..900a32b08 --- /dev/null +++ b/addon-sdk/source/test/modules/orange.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/. */ + +define(['./color'], function (color) { + return { + name: 'orange', + parentType: color.type + }; +}); diff --git a/addon-sdk/source/test/modules/pollux.js b/addon-sdk/source/test/modules/pollux.js new file mode 100644 index 000000000..61cf66f9c --- /dev/null +++ b/addon-sdk/source/test/modules/pollux.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/. */ + +define(['exports', './castor'], function(exports, castor) { + exports.name = 'pollux'; + exports.getCastorName = function () { + return castor.name; + }; +}); diff --git a/addon-sdk/source/test/modules/red.js b/addon-sdk/source/test/modules/red.js new file mode 100644 index 000000000..c47b8e924 --- /dev/null +++ b/addon-sdk/source/test/modules/red.js @@ -0,0 +1,16 @@ +/* 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/. */ + +define(function (require) { + // comment fake-outs for require finding. + // require('bad1'); + return { + name: 'red', + parentType: require('./color').type + }; + + /* + require('bad2'); + */ +}); diff --git a/addon-sdk/source/test/modules/setExports.js b/addon-sdk/source/test/modules/setExports.js new file mode 100644 index 000000000..495c301cd --- /dev/null +++ b/addon-sdk/source/test/modules/setExports.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/. */ + +module.setExports(5); diff --git a/addon-sdk/source/test/modules/subtract.js b/addon-sdk/source/test/modules/subtract.js new file mode 100644 index 000000000..06e1053ab --- /dev/null +++ b/addon-sdk/source/test/modules/subtract.js @@ -0,0 +1,9 @@ +/* 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/. */ + +define(function () { + return function (a, b) { + return a - b; + } +}); diff --git a/addon-sdk/source/test/modules/tiger.js b/addon-sdk/source/test/modules/tiger.js new file mode 100644 index 000000000..80479d019 --- /dev/null +++ b/addon-sdk/source/test/modules/tiger.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/. */ + +define(function (require, exports) { + exports.name = 'tiger'; + exports.type = require('./types/cat').type; +}); diff --git a/addon-sdk/source/test/modules/traditional1.js b/addon-sdk/source/test/modules/traditional1.js new file mode 100644 index 000000000..28af8ef30 --- /dev/null +++ b/addon-sdk/source/test/modules/traditional1.js @@ -0,0 +1,12 @@ +/* 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/. */ + +exports.name = 'traditional1' + +var async1 = require('./async1'); + +exports.traditional2Name = async1.traditional2Name; +exports.traditional1Name = async1.traditional1Name; +exports.async2Name = async1.async2Name; +exports.async2Traditional2Name = async1.async2Traditional2Name; diff --git a/addon-sdk/source/test/modules/traditional2.js b/addon-sdk/source/test/modules/traditional2.js new file mode 100644 index 000000000..67b64eecb --- /dev/null +++ b/addon-sdk/source/test/modules/traditional2.js @@ -0,0 +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/. */ + +exports.name = 'traditional2'; +exports.traditional1Name = require('./traditional1').name; diff --git a/addon-sdk/source/test/modules/types/cat.js b/addon-sdk/source/test/modules/types/cat.js new file mode 100644 index 000000000..41513d682 --- /dev/null +++ b/addon-sdk/source/test/modules/types/cat.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/. */ + +exports.type = 'cat'; diff --git a/addon-sdk/source/test/page-mod/helpers.js b/addon-sdk/source/test/page-mod/helpers.js new file mode 100644 index 000000000..3aa3deb0d --- /dev/null +++ b/addon-sdk/source/test/page-mod/helpers.js @@ -0,0 +1,117 @@ +/* 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, Ci } = require("chrome"); +const { setTimeout } = require("sdk/timers"); +const { Loader } = require("sdk/test/loader"); +const { openTab, getBrowserForTab, closeTab } = require("sdk/tabs/utils"); +const { getMostRecentBrowserWindow } = require("sdk/window/utils"); +const { merge } = require("sdk/util/object"); +const httpd = require("../lib/httpd"); +const { cleanUI } = require("sdk/test/utils"); + +const PORT = 8099; +const PATH = '/test-contentScriptWhen.html'; + +function createLoader () { + let options = merge({}, require('@loader/options'), + { id: "testloader", prefixURI: require('../fixtures').url() }); + return Loader(module, null, options); +} +exports.createLoader = createLoader; + +function openNewTab(url) { + return openTab(getMostRecentBrowserWindow(), url, { + inBackground: false + }); +} +exports.openNewTab = openNewTab; + +// an evil function enables the creation of tests +// that depend on delicate event timing. do not use. +function testPageMod(assert, done, testURL, pageModOptions, + testCallback, timeout) { + let loader = createLoader(); + let { PageMod } = loader.require("sdk/page-mod"); + let pageMods = pageModOptions.map(opts => new PageMod(opts)); + let newTab = openNewTab(testURL); + let b = getBrowserForTab(newTab); + + function onPageLoad() { + b.removeEventListener("load", onPageLoad, true); + // Delay callback execute as page-mod content scripts may be executed on + // load event. So page-mod actions may not be already done. + // If we delay even more contentScriptWhen:'end', we may want to modify + // this code again. + setTimeout(testCallback, timeout, + b.contentWindow.wrappedJSObject, // TODO: remove this CPOW + function () { + pageMods.forEach(mod => mod.destroy()); + // XXX leaks reported if we don't close the tab? + closeTab(newTab); + loader.unload(); + done(); + } + ); + } + b.addEventListener("load", onPageLoad, true); + + return pageMods; +} +exports.testPageMod = testPageMod; + +/** + * helper function that creates a PageMod and calls back the appropriate handler + * based on the value of document.readyState at the time contentScript is attached + */ +exports.handleReadyState = function(url, contentScriptWhen, callbacks) { + const loader = Loader(module); + const { PageMod } = loader.require('sdk/page-mod'); + + let pagemod = PageMod({ + include: url, + attachTo: ['existing', 'top'], + contentScriptWhen: contentScriptWhen, + contentScript: "self.postMessage(document.readyState)", + onAttach: worker => { + let { tab } = worker; + worker.on('message', readyState => { + // generate event name from `readyState`, e.g. `"loading"` becomes `onLoading`. + let type = 'on' + readyState[0].toUpperCase() + readyState.substr(1); + + if (type in callbacks) + callbacks[type](tab); + + pagemod.destroy(); + loader.unload(); + }) + } + }); +} + +// serves a slow page which takes 1.5 seconds to load, +// 0.5 seconds in each readyState: uninitialized, loading, interactive. +function contentScriptWhenServer() { + const URL = 'http://localhost:' + PORT + PATH; + + const HTML = `/* polyglot js + + delay both the "DOMContentLoaded" + + and "load" events */`; + + let srv = httpd.startServerAsync(PORT); + + srv.registerPathHandler(PATH, (_, response) => { + response.processAsync(); + response.setHeader('Content-Type', 'text/html', false); + setTimeout(_ => response.finish(), 500); + response.write(HTML); + }) + + srv.URL = URL; + return srv; +} +exports.contentScriptWhenServer = contentScriptWhenServer; diff --git a/addon-sdk/source/test/path/test-path.js b/addon-sdk/source/test/path/test-path.js new file mode 100644 index 000000000..38037af20 --- /dev/null +++ b/addon-sdk/source/test/path/test-path.js @@ -0,0 +1,430 @@ +// 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. + +// 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('sdk/test').run(exports); diff --git a/addon-sdk/source/test/preferences/common.json b/addon-sdk/source/test/preferences/common.json new file mode 100644 index 000000000..c645e24d5 --- /dev/null +++ b/addon-sdk/source/test/preferences/common.json @@ -0,0 +1,16 @@ +{ + "browser.dom.window.dump.enabled": true, + "javascript.options.showInConsole": true, + "devtools.debugger.remote-enabled": true, + "extensions.sdk.console.logLevel": "info", + "extensions.checkCompatibility.nightly": false, + "extensions.update.enabled": false, + "lightweightThemes.update.enabled": false, + "extensions.update.notifyUser": false, + "extensions.enabledScopes": 5, + "extensions.getAddons.cache.enabled": false, + "extensions.installDistroAddons": false, + "extensions.autoDisableScopes": 10, + "app.releaseNotesURL": "http://localhost/app-dummy/", + "app.vendorURL": "http://localhost/app-dummy/" +} diff --git a/addon-sdk/source/test/preferences/e10s-off.json b/addon-sdk/source/test/preferences/e10s-off.json new file mode 100644 index 000000000..269d7a92a --- /dev/null +++ b/addon-sdk/source/test/preferences/e10s-off.json @@ -0,0 +1,5 @@ +{ + "browser.tabs.remote.autostart": false, + "browser.tabs.remote.autostart.1": false, + "browser.tabs.remote.autostart.2": false +} diff --git a/addon-sdk/source/test/preferences/e10s-on.json b/addon-sdk/source/test/preferences/e10s-on.json new file mode 100644 index 000000000..dd5a78dbf --- /dev/null +++ b/addon-sdk/source/test/preferences/e10s-on.json @@ -0,0 +1,3 @@ +{ + "browser.tabs.remote.autostart": true +} diff --git a/addon-sdk/source/test/preferences/firefox.json b/addon-sdk/source/test/preferences/firefox.json new file mode 100644 index 000000000..5b8145cec --- /dev/null +++ b/addon-sdk/source/test/preferences/firefox.json @@ -0,0 +1,11 @@ +{ + "browser.startup.homepage": "about:blank", + "startup.homepage_welcome_url": "about:blank", + "devtools.browsertoolbox.panel": "jsdebugger", + "devtools.chrome.enabled": true, + "urlclassifier.updateinterval": 172800, + "browser.safebrowsing.provider.google.gethashURL": "http://localhost/safebrowsing-dummy/gethash", + "browser.safebrowsing.provider.google.updateURL": "http://localhost/safebrowsing-dummy/update", + "browser.safebrowsing.provider.mozilla.gethashURL": "http://localhost/safebrowsing-dummy/gethash", + "browser.safebrowsing.provider.mozilla.updateURL": "http://localhost/safebrowsing-dummy/update" +} diff --git a/addon-sdk/source/test/preferences/no-connections.json b/addon-sdk/source/test/preferences/no-connections.json new file mode 100644 index 000000000..370b7909c --- /dev/null +++ b/addon-sdk/source/test/preferences/no-connections.json @@ -0,0 +1,41 @@ +{ + "toolkit.telemetry.enabled": false, + "toolkit.telemetry.server": "https://localhost/telemetry-dummy/", + "app.update.auto": false, + "app.update.url": "http://localhost/app-dummy/update", + "app.update.enabled": false, + "app.update.staging.enabled": false, + "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", + "media.gmp-manager.updateEnabled": false, + "browser.aboutHomeSnippets.updateUrl": "https://localhost/snippet-dummy", + "browser.newtab.url": "about:blank", + "browser.search.update": false, + "browser.search.suggest.enabled": false, + "browser.safebrowsing.phishing.enabled": false, + "browser.safebrowsing.provider.google.updateURL": "http://localhost/safebrowsing-dummy/update", + "browser.safebrowsing.provider.google.gethashURL": "http://localhost/safebrowsing-dummy/gethash", + "browser.safebrowsing.provider.google.reportURL": "http://localhost/safebrowsing-dummy/malwarereport", + "browser.selfsupport.url": "https://localhost/selfsupport-dummy", + "browser.safebrowsing.provider.mozilla.gethashURL": "http://localhost/safebrowsing-dummy/gethash", + "browser.safebrowsing.provider.mozilla.updateURL": "http://localhost/safebrowsing-dummy/update", + "browser.newtabpage.directory.source": "data:application/json,{'jetpack':1}", + "browser.newtabpage.directory.ping": "", + "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", + "extensions.webservice.discoverURL": "http://localhost/extensions-dummy/discoveryURL", + "extensions.getAddons.maxResults": 0, + "services.blocklist.base": "http://localhost/dummy-kinto/v1", + "geo.wifi.uri": "http://localhost/location-dummy/locationURL", + "browser.search.geoip.url": "http://localhost/location-dummy/locationURL", + "browser.search.isUS": true, + "browser.search.countryCode": "US", + "geo.wifi.uri": "http://localhost/extensions-dummy/geowifiURL", + "geo.wifi.scan": false, + "browser.webapps.checkForUpdates": 0, + "identity.fxaccounts.auth.uri": "http://localhost/fxa-dummy/" +} diff --git a/addon-sdk/source/test/preferences/test-e10s-preferences.js b/addon-sdk/source/test/preferences/test-e10s-preferences.js new file mode 100644 index 000000000..ed09e5fa3 --- /dev/null +++ b/addon-sdk/source/test/preferences/test-e10s-preferences.js @@ -0,0 +1,15 @@ +/* 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 extend = require("lodash").extend + +var e10sOn = require("./e10s-on.json"); +var commonPrefs = require("./common.json"); +var testPrefs = require("./test.json"); +var fxPrefs = require("./firefox.json"); +var disconnectionPrefs = require("./no-connections.json"); + +var prefs = extend({}, e10sOn, commonPrefs, testPrefs, disconnectionPrefs, fxPrefs); +module.exports = prefs; diff --git a/addon-sdk/source/test/preferences/test-preferences.js b/addon-sdk/source/test/preferences/test-preferences.js new file mode 100644 index 000000000..5cdb4a292 --- /dev/null +++ b/addon-sdk/source/test/preferences/test-preferences.js @@ -0,0 +1,15 @@ +/* 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 extend = require("lodash").extend + +var e10sOff = require("./e10s-off.json"); +var commonPrefs = require("./common.json"); +var testPrefs = require("./test.json"); +var fxPrefs = require("./firefox.json"); +var disconnectionPrefs = require("./no-connections.json"); + +var prefs = extend({}, e10sOff, commonPrefs, testPrefs, disconnectionPrefs, fxPrefs); +module.exports = prefs; diff --git a/addon-sdk/source/test/preferences/test.json b/addon-sdk/source/test/preferences/test.json new file mode 100644 index 000000000..d34061fb8 --- /dev/null +++ b/addon-sdk/source/test/preferences/test.json @@ -0,0 +1,46 @@ +{ + "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, + "network.http.speculative-parallel-limit": 0, + "test.mousescroll": true, + "security.default_personal_cert": "Select Automatically", + "network.http.prompt-temp-redirect": false, + "security.warn_viewing_mixed": false, + "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, + "network.http.bypass-cachelock-threshold": 200000, + "geo.provider.testing": true, + "browser.pagethumbnails.capturing_disabled": true, + "browser.download.panel.shown": true, + "general.useragent.updates.enabled": false, + "media.eme.enabled": true, + "media.eme.apiVisible": true, + "dom.ipc.tabs.shutdownTimeoutSecs": 0, + "general.useragent.locale": "en-US", + "intl.locale.matchOS": "en-US", + "dom.indexedDB.experimental": true +} diff --git a/addon-sdk/source/test/private-browsing/helper.js b/addon-sdk/source/test/private-browsing/helper.js new file mode 100644 index 000000000..4a400b95b --- /dev/null +++ b/addon-sdk/source/test/private-browsing/helper.js @@ -0,0 +1,58 @@ +/* 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 xulApp = require("sdk/system/xul-app"); +const { open: openWindow, getMostRecentBrowserWindow } = require('sdk/window/utils'); +const { openTab, getTabContentWindow, getActiveTab, setTabURL, closeTab } = require('sdk/tabs/utils'); +const promise = require("sdk/core/promise"); +const windowHelpers = require('sdk/window/helpers'); +const events = require("sdk/system/events"); + +exports.openWebpage = function openWebpage(url, enablePrivate) { + if (xulApp.is("Fennec")) { + let chromeWindow = getMostRecentBrowserWindow(); + let rawTab = openTab(chromeWindow, url, { + isPrivate: enablePrivate + }); + + return { + ready: promise.resolve(getTabContentWindow(rawTab)), + close: function () { + closeTab(rawTab); + // Returns a resolved promise as there is no need to wait + return promise.resolve(); + } + }; + } + else { + let win = openWindow(null, { + features: { + private: enablePrivate + } + }); + let deferred = promise.defer(); + + // Wait for delayed startup code to be executed, in order to ensure + // that the window is really ready + events.on("browser-delayed-startup-finished", function onReady({subject}) { + if (subject == win) { + events.off("browser-delayed-startup-finished", onReady); + deferred.resolve(win); + + let rawTab = getActiveTab(win); + setTabURL(rawTab, url); + deferred.resolve(getTabContentWindow(rawTab)); + } + }, true); + + return { + ready: deferred.promise, + close: function () { + return windowHelpers.close(win); + } + }; + } + return null; +} diff --git a/addon-sdk/source/test/private-browsing/tabs.js b/addon-sdk/source/test/private-browsing/tabs.js new file mode 100644 index 000000000..8564f0735 --- /dev/null +++ b/addon-sdk/source/test/private-browsing/tabs.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'; + +const { Ci } = require('chrome'); +const { openTab, closeTab } = require('sdk/tabs/utils'); +const { browserWindows } = require('sdk/windows'); +const { isPrivate } = require('sdk/private-browsing'); + +exports.testIsPrivateOnTab = function(assert) { + let window = browserWindows.activeWindow; + assert.ok(!isPrivate(chromeWindow), 'the top level window is not private'); + + let rawTab = openTab(chromeWindow, 'data:text/html,

Hi!

', { + isPrivate: true + }); + + // test that the tab is private + assert.ok(rawTab.browser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing); + assert.ok(isPrivate(rawTab.browser.contentWindow)); + assert.ok(isPrivate(rawTab.browser)); + + closeTab(rawTab); +}; diff --git a/addon-sdk/source/test/private-browsing/windows.js b/addon-sdk/source/test/private-browsing/windows.js new file mode 100644 index 000000000..e6f9c53b5 --- /dev/null +++ b/addon-sdk/source/test/private-browsing/windows.js @@ -0,0 +1,115 @@ +/* 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 { onFocus, openDialog, open } = require('sdk/window/utils'); +const { open: openPromise, close, focus, promise } = require('sdk/window/helpers'); +const { isPrivate } = require('sdk/private-browsing'); +const { getMode } = require('sdk/private-browsing/utils'); +const { browserWindows: windows } = require('sdk/windows'); +const { defer } = require('sdk/core/promise'); +const tabs = require('sdk/tabs'); +const { getMostRecentBrowserWindow } = require('sdk/window/utils'); +const { cleanUI } = require("sdk/test/utils"); + +// test openDialog() from window/utils with private option +// test isActive state in pwpb case +// test isPrivate on ChromeWindow +exports.testPerWindowPrivateBrowsingGetter = function*(assert) { + let win = openDialog({ private: true }); + + yield promise(win, 'DOMContentLoaded'); + + assert.equal(getMode(win), true, 'Newly opened window is in PB mode'); + assert.ok(isPrivate(win), 'isPrivate(window) is true'); + + yield close(win); +} + +// test open() from window/utils with private feature +// test isActive state in pwpb case +// test isPrivate on ChromeWindow +exports.testPerWindowPrivateBrowsingGetter = function*(assert) { + let win = open('chrome://browser/content/browser.xul', { + features: { + private: true + } + }); + + yield promise(win, 'DOMContentLoaded'); + assert.equal(getMode(win), true, 'Newly opened window is in PB mode'); + assert.ok(isPrivate(win), 'isPrivate(window) is true'); + yield close(win) +} + +exports.testIsPrivateOnWindowOpen = function*(assert) { + let window = yield new Promise(resolve => { + windows.open({ + isPrivate: true, + onOpen: resolve + }); + }); + + assert.equal(isPrivate(window), false, 'isPrivate for a window is true when it should be'); + assert.equal(isPrivate(window.tabs[0]), false, 'isPrivate for a tab is false when it should be'); + + yield cleanUI(); +} + +exports.testIsPrivateOnWindowOpenFromPrivate = function(assert, done) { + // open a private window + openPromise(null, { + features: { + private: true, + chrome: true, + titlebar: true, + toolbar: true + } + }).then(focus).then(function(window) { + let { promise, resolve } = defer(); + + assert.equal(isPrivate(window), true, 'the only open window is private'); + + windows.open({ + url: 'about:blank', + onOpen: function(w) { + assert.equal(isPrivate(w), false, 'new test window is not private'); + w.close(() => resolve(window)); + } + }); + + return promise; + }).then(close). + then(done, assert.fail); +}; + +exports.testOpenTabWithPrivateWindow = function*(assert) { + let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true }); + + assert.pass("loading new private window"); + + yield promise(window, 'load').then(focus); + + assert.equal(isPrivate(window), true, 'the focused window is private'); + + yield new Promise(resolve => tabs.open({ + url: 'about:blank', + onOpen: (tab) => { + assert.equal(isPrivate(tab), false, 'the opened tab is not private'); + tab.close(resolve); + } + })); + + yield close(window); +}; + +exports.testIsPrivateOnWindowOff = function(assert, done) { + windows.open({ + onOpen: function(window) { + assert.equal(isPrivate(window), false, 'isPrivate for a window is false when it should be'); + assert.equal(isPrivate(window.tabs[0]), false, 'isPrivate for a tab is false when it should be'); + window.close(done); + } + }) +} diff --git a/addon-sdk/source/test/querystring/test-querystring.js b/addon-sdk/source/test/querystring/test-querystring.js new file mode 100644 index 000000000..9a4fbe573 --- /dev/null +++ b/addon-sdk/source/test/querystring/test-querystring.js @@ -0,0 +1,205 @@ +// 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. + +"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('sdk/test').run(exports); diff --git a/addon-sdk/source/test/sidebar/utils.js b/addon-sdk/source/test/sidebar/utils.js new file mode 100644 index 000000000..21002ba49 --- /dev/null +++ b/addon-sdk/source/test/sidebar/utils.js @@ -0,0 +1,74 @@ +/* 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 = { + 'engines': { + 'Firefox': '*' + } +}; + +const { Cu } = require('chrome'); +const { getMostRecentBrowserWindow } = require('sdk/window/utils'); +const { fromIterator } = require('sdk/util/array'); + +const BUILTIN_SIDEBAR_MENUITEMS = exports.BUILTIN_SIDEBAR_MENUITEMS = [ + 'menu_socialSidebar', + 'menu_historySidebar', + 'menu_bookmarksSidebar', + 'menu_tabsSidebar', +]; + +function isSidebarShowing(window) { + window = window || getMostRecentBrowserWindow(); + let sidebar = window.document.getElementById('sidebar-box'); + return !sidebar.hidden; +} +exports.isSidebarShowing = isSidebarShowing; + +function getSidebarMenuitems(window) { + window = window || getMostRecentBrowserWindow(); + return fromIterator(window.document.querySelectorAll('#viewSidebarMenu menuitem')); +} +exports.getSidebarMenuitems = getSidebarMenuitems; + +function getExtraSidebarMenuitems() { + let menuitems = getSidebarMenuitems(); + return menuitems.filter(function(mi) { + return BUILTIN_SIDEBAR_MENUITEMS.indexOf(mi.getAttribute('id')) < 0; + }); +} +exports.getExtraSidebarMenuitems = getExtraSidebarMenuitems; + +function makeID(id) { + return 'jetpack-sidebar-' + id; +} +exports.makeID = makeID; + +function simulateCommand(ele) { + let window = ele.ownerDocument.defaultView; + let { document } = window; + var evt = document.createEvent('XULCommandEvent'); + evt.initCommandEvent('command', true, true, window, + 0, false, false, false, false, null); + ele.dispatchEvent(evt); +} +exports.simulateCommand = simulateCommand; + +function simulateClick(ele) { + let window = ele.ownerDocument.defaultView; + let { document } = window; + let evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + ele.dispatchEvent(evt); +} +exports.simulateClick = simulateClick; + +// OSX and Windows exhibit different behaviors when 'checked' is false, +// so compare against the consistent 'true'. See bug 894809. +function isChecked(node) { + return node.getAttribute('checked') === 'true'; +}; +exports.isChecked = isChecked; diff --git a/addon-sdk/source/test/tabs/test-fennec-tabs.js b/addon-sdk/source/test/tabs/test-fennec-tabs.js new file mode 100644 index 000000000..d7e362d41 --- /dev/null +++ b/addon-sdk/source/test/tabs/test-fennec-tabs.js @@ -0,0 +1,595 @@ +/* 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, Ci } = require('chrome'); +const { Loader, LoaderWithHookedConsole } = require('sdk/test/loader'); +const timer = require('sdk/timers'); +const tabs = require('sdk/tabs'); +const windows = require('sdk/windows'); +const { set: setPref } = require("sdk/preferences/service"); +const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings"; + +const tabsLen = tabs.length; +const URL = 'data:text/html;charset=utf-8,#title#'; + +// Fennec error message dispatched on all currently unimplement tab features, +// that match LoaderWithHookedConsole messages object pattern +const ERR_FENNEC_MSG = { + type: "error", + msg: "This method is not yet supported by Fennec" +}; + +// TEST: tab unloader +exports.testAutomaticDestroy = function(assert, done) { + let called = false; + + let loader2 = Loader(module); + let loader3 = Loader(module); + let tabs2 = loader2.require('sdk/tabs'); + let tabs3 = loader3.require('sdk/tabs'); + let tabs2Len = tabs2.length; + + tabs2.on('open', function onOpen(tab) { + assert.fail("an onOpen listener was called that should not have been"); + called = true; + }); + tabs2.on('ready', function onReady(tab) { + assert.fail("an onReady listener was called that should not have been"); + called = true; + }); + tabs2.on('select', function onSelect(tab) { + assert.fail("an onSelect listener was called that should not have been"); + called = true; + }); + tabs2.on('close', function onClose(tab) { + assert.fail("an onClose listener was called that should not have been"); + called = true; + }); + loader2.unload(); + + tabs3.on('open', function onOpen(tab) { + assert.pass("an onOpen listener was called for tabs3"); + + tab.on('ready', function onReady(tab) { + assert.fail("an onReady listener was called that should not have been"); + called = true; + }); + tab.on('select', function onSelect(tab) { + assert.fail("an onSelect listener was called that should not have been"); + called = true; + }); + tab.on('close', function onClose(tab) { + assert.fail("an onClose listener was called that should not have been"); + called = true; + }); + }); + tabs3.open(URL.replace(/#title#/, 'tabs3')); + loader3.unload(); + + // Fire a tab event and ensure that the destroyed tab is inactive + tabs.once('open', function(tab) { + assert.pass('tabs.once("open") works!'); + + assert.equal(tabs2Len, tabs2.length, "tabs2 length was not changed"); + assert.equal(tabs.length, (tabs2.length+2), "tabs.length > tabs2.length"); + + tab.once('ready', function() { + assert.pass('tab.once("ready") works!'); + + tab.once('close', function() { + assert.pass('tab.once("close") works!'); + + timer.setTimeout(function () { + assert.ok(!called, "Unloaded tab module is destroyed and inactive"); + + // end test + done(); + }); + }); + + tab.close(); + }); + }); + + tabs.open('data:text/html;charset=utf-8,foo'); +}; + +// TEST: tab properties +exports.testTabProperties = function(assert, done) { + let url = "data:text/html;charset=utf-8,foofoo"; + let tabsLen = tabs.length; + tabs.open({ + url: url, + onReady: function(tab) { + assert.equal(tab.title, "foo", "title of the new tab matches"); + assert.equal(tab.url, url, "URL of the new tab matches"); + assert.equal(tab.style, null, "style of the new tab matches"); + assert.equal(tab.index, tabsLen, "index of the new tab matches"); + assert.notEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches"); + assert.notEqual(tab.id, null, "a tab object always has an id property"); + + tab.close(function() { + loader.unload(); + + // end test + done(); + }); + } + }); +}; + +// TEST: tabs iterator and length property +exports.testTabsIteratorAndLength = function(assert, done) { + let newTabs = []; + let startCount = 0; + for (let t of tabs) startCount++; + + assert.equal(startCount, tabs.length, "length property is correct"); + + let url = "data:text/html;charset=utf-8,testTabsIteratorAndLength"; + tabs.open({url: url, onOpen: tab => newTabs.push(tab)}); + tabs.open({url: url, onOpen: tab => newTabs.push(tab)}); + tabs.open({ + url: url, + onOpen: function(tab) { + let count = 0; + for (let t of tabs) count++; + assert.equal(count, startCount + 3, "iterated tab count matches"); + assert.equal(startCount + 3, tabs.length, "iterated tab count matches length property"); + + let newTabsLength = newTabs.length; + newTabs.forEach(t => t.close(function() { + if (--newTabsLength > 0) return; + + tab.close(done); + })); + } + }); +}; + +// TEST: tab.url setter +exports.testTabLocation = function(assert, done) { + let url1 = "data:text/html;charset=utf-8,foo"; + let url2 = "data:text/html;charset=utf-8,bar"; + + tabs.on('ready', function onReady(tab) { + if (tab.url != url2) + return; + + tabs.removeListener('ready', onReady); + assert.pass("tab loaded the correct url"); + + tab.close(done); + }); + + tabs.open({ + url: url1, + onOpen: function(tab) { + tab.url = url2; + } + }); +}; + +// TEST: tab.move() +exports.testTabMove = function(assert, done) { + let { loader, messages } = LoaderWithHookedConsole(); + let tabs = loader.require('sdk/tabs'); + + let url = "data:text/html;charset=utf-8,testTabMove"; + + tabs.open({ + url: url, + onOpen: function(tab1) { + assert.ok(tab1.index >= 0, "opening a tab returns a tab w/ valid index"); + + tabs.open({ + url: url, + onOpen: function(tab) { + let i = tab.index; + assert.ok(tab.index > tab1.index, "2nd tab has valid index"); + tab.index = 0; + assert.equal(tab.index, i, "tab index after move matches"); + assert.equal(JSON.stringify(messages), + JSON.stringify([ERR_FENNEC_MSG]), + "setting tab.index logs error"); + // end test + tab1.close(() => tab.close(function() { + loader.unload(); + done(); + })); + } + }); + } + }); +}; + +// TEST: open tab with default options +exports.testTabsOpen_alt = function(assert, done) { + let { loader, messages } = LoaderWithHookedConsole(); + let tabs = loader.require('sdk/tabs'); + let url = "data:text/html;charset=utf-8,default"; + + tabs.open({ + url: url, + onReady: function(tab) { + assert.equal(tab.url, url, "URL of the new tab matches"); + assert.equal(tabs.activeTab, tab, "URL of active tab in the current window matches"); + assert.equal(tab.isPinned, false, "The new tab is not pinned"); + assert.equal(messages.length, 1, "isPinned logs error"); + + // end test + tab.close(function() { + loader.unload(); + done(); + }); + } + }); +}; + +// TEST: open pinned tab +exports.testOpenPinned_alt = function(assert, done) { + let { loader, messages } = LoaderWithHookedConsole(); + let tabs = loader.require('sdk/tabs'); + let url = "about:blank"; + + tabs.open({ + url: url, + isPinned: true, + onOpen: function(tab) { + assert.equal(tab.isPinned, false, "The new tab is pinned"); + // We get two error message: one for tabs.open's isPinned argument + // and another one for tab.isPinned + assert.equal(JSON.stringify(messages), + JSON.stringify([ERR_FENNEC_MSG, ERR_FENNEC_MSG]), + "isPinned logs error"); + + // end test + tab.close(function() { + loader.unload(); + done(); + }); + } + }); +}; + +// TEST: pin/unpin opened tab +exports.testPinUnpin_alt = function(assert, done) { + let { loader, messages } = LoaderWithHookedConsole(); + let tabs = loader.require('sdk/tabs'); + let url = "data:text/html;charset=utf-8,default"; + + tabs.open({ + url: url, + onOpen: function(tab) { + tab.pin(); + assert.equal(tab.isPinned, false, "The tab was pinned correctly"); + assert.equal(JSON.stringify(messages), + JSON.stringify([ERR_FENNEC_MSG, ERR_FENNEC_MSG]), + "tab.pin() logs error"); + + // Clear console messages for the following test + messages.length = 0; + + tab.unpin(); + assert.equal(tab.isPinned, false, "The tab was unpinned correctly"); + assert.equal(JSON.stringify(messages), + JSON.stringify([ERR_FENNEC_MSG, ERR_FENNEC_MSG]), + "tab.unpin() logs error"); + + // end test + tab.close(function() { + loader.unload(); + done(); + }); + } + }); +}; + +// TEST: open tab in background +exports.testInBackground = function(assert, done) { + let activeUrl = tabs.activeTab.url; + let url = "data:text/html;charset=utf-8,background"; + let window = windows.browserWindows.activeWindow; + tabs.once('ready', function onReady(tab) { + assert.equal(tabs.activeTab.url, activeUrl, "URL of active tab has not changed"); + assert.equal(tab.url, url, "URL of the new background tab matches"); + assert.equal(windows.browserWindows.activeWindow, window, "a new window was not opened"); + assert.notEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL"); + + // end test + tab.close(done); + }); + + tabs.open({ + url: url, + inBackground: true + }); +}; + +// TEST: open tab in new window +exports.testOpenInNewWindow = function(assert, done) { + let url = "data:text/html;charset=utf-8,newwindow"; + let window = windows.browserWindows.activeWindow; + + tabs.open({ + url: url, + inNewWindow: true, + onReady: function(tab) { + assert.equal(windows.browserWindows.length, 1, "a new window was not opened"); + assert.equal(windows.browserWindows.activeWindow, window, "old window is active"); + assert.equal(tab.url, url, "URL of the new tab matches"); + assert.equal(tabs.activeTab, tab, "tab is the activeTab"); + + tab.close(done); + } + }); +}; + +// TEST: onOpen event handler +exports.testTabsEvent_onOpen = function(assert, done) { + let url = URL.replace('#title#', 'testTabsEvent_onOpen'); + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('open', listener1); + + // add listener via collection add + tabs.on('open', function listener2(tab) { + assert.equal(++eventCount, 2, "both listeners notified"); + tabs.removeListener('open', listener1); + tabs.removeListener('open', listener2); + + // ends test + tab.close(done); + }); + + tabs.open(url); +}; + +// TEST: onClose event handler +exports.testTabsEvent_onClose = function(assert, done) { + let url = "data:text/html;charset=utf-8,onclose"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + } + tabs.on('close', listener1); + + // add listener via collection add + tabs.on('close', function listener2(tab) { + assert.equal(++eventCount, 2, "both listeners notified"); + tabs.removeListener('close', listener1); + tabs.removeListener('close', listener2); + + // end test + done(); + }); + + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + tab.close(); + }); + + tabs.open(url); +}; + +// TEST: onClose event handler when a window is closed +exports.testTabsEvent_onCloseWindow = function(assert, done) { + let closeCount = 0, individualCloseCount = 0; + function listener() { + closeCount++; + } + tabs.on('close', listener); + + // One tab is already open with the window + let openTabs = 0; + function testCasePossiblyLoaded(tab) { + tab.close(function() { + if (++openTabs == 3) { + tabs.removeListener("close", listener); + + assert.equal(closeCount, 3, "Correct number of close events received"); + assert.equal(individualCloseCount, 3, + "Each tab with an attached onClose listener received a close " + + "event when the window was closed"); + + done(); + } + }); + } + + tabs.open({ + url: "data:text/html;charset=utf-8,tab2", + onOpen: testCasePossiblyLoaded, + onClose: () => individualCloseCount++ + }); + + tabs.open({ + url: "data:text/html;charset=utf-8,tab3", + onOpen: testCasePossiblyLoaded, + onClose: () => individualCloseCount++ + }); + + tabs.open({ + url: "data:text/html;charset=utf-8,tab4", + onOpen: testCasePossiblyLoaded, + onClose: () => individualCloseCount++ + }); +}; + +// TEST: onReady event handler +exports.testTabsEvent_onReady = function(assert, done) { + let url = "data:text/html;charset=utf-8,onready"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('ready', listener1); + + // add listener via collection add + tabs.on('ready', function listener2(tab) { + assert.equal(++eventCount, 2, "both listeners notified"); + tabs.removeListener('ready', listener1); + tabs.removeListener('ready', listener2); + + // end test + tab.close(done); + }); + + tabs.open(url); +}; + +// TEST: onActivate event handler +exports.testTabsEvent_onActivate = function(assert, done) { + let url = "data:text/html;charset=utf-8,onactivate"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('activate', listener1); + + // add listener via collection add + tabs.on('activate', function listener2(tab) { + assert.equal(++eventCount, 2, "both listeners notified"); + assert.equal(tab, tabs.activeTab, 'the active tab is correct'); + tabs.removeListener('activate', listener1); + tabs.removeListener('activate', listener2); + + // end test + tab.close(done); + }); + + tabs.open(url); +}; + +// TEST: onDeactivate event handler +exports.testTabsEvent_onDeactivate = function(assert, done) { + let url = "data:text/html;charset=utf-8,ondeactivate"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('deactivate', listener1); + + // add listener via collection add + tabs.on('deactivate', function listener2(tab) { + assert.equal(++eventCount, 2, 'both listeners notified'); + assert.notEqual(tab, tabs.activeTab, 'the active tab is not the deactivated tab'); + tabs.removeListener('deactivate', listener1); + tabs.removeListener('deactivate', listener2); + + // end test + tab.close(done); + }); + + tabs.on('activate', function onActivate(tab) { + tabs.removeListener('activate', onActivate); + tabs.open("data:text/html;charset=utf-8,foo"); + tab.close(); + }); + + tabs.open(url); +}; + +// TEST: per-tab event handlers +exports.testPerTabEvents = function(assert, done) { + let eventCount = 0; + + tabs.open({ + url: "data:text/html;charset=utf-8,foo", + onOpen: function(tab) { + // add listener via property assignment + function listener1() { + eventCount++; + }; + tab.on('ready', listener1); + + // add listener via collection add + tab.on('ready', function listener2() { + assert.equal(eventCount, 1, "both listeners notified"); + tab.removeListener('ready', listener1); + tab.removeListener('ready', listener2); + + // end test + tab.close(done); + }); + } + }); +}; + +exports.testUniqueTabIds = function(assert, done) { + var tabs = require('sdk/tabs'); + var tabIds = {}; + var steps = [ + function (index) { + tabs.open({ + url: "data:text/html;charset=utf-8,foo", + onOpen: function(tab) { + tabIds['tab1'] = tab.id; + next(index); + } + }); + }, + function (index) { + tabs.open({ + url: "data:text/html;charset=utf-8,bar", + onOpen: function(tab) { + tabIds['tab2'] = tab.id; + next(index); + } + }); + }, + function (index) { + assert.notEqual(tabIds.tab1, tabIds.tab2, "Tab ids should be unique."); + done(); + } + ]; + + function next(index) { + if (index === steps.length) { + return; + } + let fn = steps[index]; + index++; + fn(index); + } + + next(0); +} + +exports.testOnLoadEventWithDOM = function(assert, done) { + let count = 0; + let title = 'testOnLoadEventWithDOM'; + + tabs.open({ + url: 'data:text/html;charset=utf-8,' + title + '', + inBackground: true, + onLoad: function(tab) { + assert.equal(tab.title, title, 'tab passed in as arg, load called'); + + if (++count > 1) { + assert.pass('onLoad event called on reload'); + tab.close(done); + } + else { + assert.pass('first onLoad event occured'); + tab.reload(); + } + } + }); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/tabs/test-firefox-tabs.js b/addon-sdk/source/test/tabs/test-firefox-tabs.js new file mode 100644 index 000000000..368ed02ba --- /dev/null +++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js @@ -0,0 +1,1305 @@ +/* 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, Ci } = require('chrome'); +const { Loader } = require('sdk/test/loader'); +const systemEvents = require("sdk/system/events"); +const { setTimeout, setImmediate } = require('sdk/timers'); +const { modelFor } = require('sdk/model/core'); +const { viewFor } = require('sdk/view/core'); +const { getOwnerWindow } = require('sdk/tabs/utils'); +const { windows, onFocus, getMostRecentBrowserWindow } = require('sdk/window/utils'); +const { open, focus, close } = require('sdk/window/helpers'); +const { observer: windowObserver } = require("sdk/windows/observer"); +const tabs = require('sdk/tabs'); +const { browserWindows } = require('sdk/windows'); +const { set: setPref, get: getPref, reset: resetPref } = require("sdk/preferences/service"); +const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings"; +const OPEN_IN_NEW_WINDOW_PREF = 'browser.link.open_newwindow'; +const DISABLE_POPUP_PREF = 'dom.disable_open_during_load'; +const fixtures = require("../fixtures"); +const { base64jpeg } = fixtures; +const { cleanUI, before, after } = require("sdk/test/utils"); +const { wait } = require('../event/helpers'); + +// Bug 682681 - tab.title should never be empty +exports.testBug682681_aboutURI = function(assert, done) { + let url = 'chrome://browser/locale/tabbrowser.properties'; + let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(url); + let emptyTabTitle = stringBundle.GetStringFromName('tabs.emptyTabTitle'); + + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + + assert.equal(tab.title, + emptyTabTitle, + "title of about: tab is not blank"); + + tab.close(done); + }); + + // open a about: url + tabs.open({ + url: "about:blank", + inBackground: true + }); +}; + +// related to Bug 682681 +exports.testTitleForDataURI = function(assert, done) { + tabs.open({ + url: "data:text/html;charset=utf-8,tab", + inBackground: true, + onReady: function(tab) { + assert.equal(tab.title, "tab", "data: title is not Connecting..."); + tab.close(done); + } + }); +}; + +// TEST: 'BrowserWindow' instance creation on tab 'activate' event +// See bug 648244: there was a infinite loop. +exports.testBrowserWindowCreationOnActivate = function(assert, done) { + let windows = require("sdk/windows").browserWindows; + let gotActivate = false; + + tabs.once('activate', function onActivate(eventTab) { + assert.ok(windows.activeWindow, "Is able to fetch activeWindow"); + gotActivate = true; + }); + + open().then(function(window) { + assert.ok(gotActivate, "Received activate event"); + return close(window); + }).then(done).then(null, assert.fail); +} + +// TEST: tab unloader +exports.testAutomaticDestroyEventOpen = function(assert, done) { + let called = false; + let loader = Loader(module); + let tabs2 = loader.require("sdk/tabs"); + tabs2.on('open', _ => called = true); + + // Fire a tab event and ensure that the destroyed tab is inactive + tabs.once('open', tab => { + setTimeout(_ => { + assert.ok(!called, "Unloaded tab module is destroyed and inactive"); + tab.close(done); + }); + }); + + loader.unload(); + tabs.open("data:text/html;charset=utf-8,testAutomaticDestroyEventOpen"); +}; + +exports.testAutomaticDestroyEventActivate = function(assert, done) { + let called = false; + let loader = Loader(module); + let tabs2 = loader.require("sdk/tabs"); + tabs2.on('activate', _ => called = true); + + // Fire a tab event and ensure that the destroyed tab is inactive + tabs.once('activate', tab => { + setTimeout(_ => { + assert.ok(!called, "Unloaded tab module is destroyed and inactive"); + tab.close(done); + }); + }); + + loader.unload(); + tabs.open("data:text/html;charset=utf-8,testAutomaticDestroyEventActivate"); +}; + +exports.testAutomaticDestroyEventDeactivate = function(assert, done) { + let called = false; + let currentTab = tabs.activeTab; + let loader = Loader(module); + let tabs2 = loader.require("sdk/tabs"); + + tabs.open({ + url: "data:text/html;charset=utf-8,testAutomaticDestroyEventDeactivate", + onActivate: _ => setTimeout(_ => { + tabs2.on('deactivate', _ => called = true); + + // Fire a tab event and ensure that the destroyed tab is inactive + tabs.once('deactivate', tab => { + setTimeout(_ => { + assert.ok(!called, "Unloaded tab module is destroyed and inactive"); + tab.close(done); + }); + }); + + loader.unload(); + currentTab.activate(); + }) + }); +}; + +exports.testAutomaticDestroyEventClose = function(assert, done) { + let called = false; + let loader = Loader(module); + let tabs2 = loader.require("sdk/tabs"); + + tabs.open({ + url: "data:text/html;charset=utf-8,testAutomaticDestroyEventClose", + onReady: tab => { + tabs2.on('close', _ => called = true); + + // Fire a tab event and ensure that the destroyed tab is inactive + tabs.once('close', tab => { + setTimeout(_ => { + assert.ok(!called, "Unloaded tab module is destroyed and inactive"); + done(); + }); + }); + + loader.unload(); + tab.close(); + } + }); +}; + +exports.testTabPropertiesInNewWindow = function(assert, done) { + const { LoaderWithFilteredConsole } = require("sdk/test/loader"); + let loader = LoaderWithFilteredConsole(module, function(type, message) { + return true; + }); + + let tabs = loader.require('sdk/tabs'); + let { viewFor } = loader.require('sdk/view/core'); + + let count = 0; + function onReadyOrLoad (tab) { + if (count++) { + close(getOwnerWindow(viewFor(tab))).then(done).then(null, assert.fail); + } + } + + let url = "data:text/html;charset=utf-8,foofoo"; + tabs.open({ + inNewWindow: true, + url: url, + onReady: function(tab) { + assert.equal(tab.title, "foo", "title of the new tab matches"); + assert.equal(tab.url, url, "URL of the new tab matches"); + assert.equal(tab.favicon, undefined, "favicon of the new tab is undefined"); + assert.equal(tab.style, null, "style of the new tab matches"); + assert.equal(tab.index, 0, "index of the new tab matches"); + assert.notEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches"); + assert.notEqual(tab.id, null, "a tab object always has an id property."); + + onReadyOrLoad(tab); + }, + onLoad: function(tab) { + assert.equal(tab.title, "foo", "title of the new tab matches"); + assert.equal(tab.url, url, "URL of the new tab matches"); + assert.equal(tab.favicon, undefined, "favicon of the new tab is undefined"); + assert.equal(tab.style, null, "style of the new tab matches"); + assert.equal(tab.index, 0, "index of the new tab matches"); + assert.notEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches"); + assert.notEqual(tab.id, null, "a tab object always has an id property."); + + onReadyOrLoad(tab); + } + }); +}; + +exports.testTabPropertiesInSameWindow = function(assert, done) { + const { LoaderWithFilteredConsole } = require("sdk/test/loader"); + let loader = LoaderWithFilteredConsole(module, function(type, message) { + return true; + }); + + let tabs = loader.require('sdk/tabs'); + + // Get current count of tabs so we know the index of the + // new tab, bug 893846 + let tabCount = tabs.length; + let count = 0; + function onReadyOrLoad (tab) { + if (count++) { + tab.close(done); + } + } + + let url = "data:text/html;charset=utf-8,foofoo"; + tabs.open({ + url: url, + onReady: function(tab) { + assert.equal(tab.title, "foo", "title of the new tab matches"); + assert.equal(tab.url, url, "URL of the new tab matches"); + assert.equal(tab.favicon, undefined, "favicon of the new tab is undefined"); + assert.equal(tab.style, null, "style of the new tab matches"); + assert.equal(tab.index, tabCount, "index of the new tab matches"); + assert.notEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches"); + assert.notEqual(tab.id, null, "a tab object always has an id property."); + + onReadyOrLoad(tab); + }, + onLoad: function(tab) { + assert.equal(tab.title, "foo", "title of the new tab matches"); + assert.equal(tab.url, url, "URL of the new tab matches"); + assert.equal(tab.favicon, undefined, "favicon of the new tab is undefined"); + assert.equal(tab.style, null, "style of the new tab matches"); + assert.equal(tab.index, tabCount, "index of the new tab matches"); + assert.notEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches"); + assert.notEqual(tab.id, null, "a tab object always has an id property."); + + onReadyOrLoad(tab); + } + }); +}; + +// TEST: tab properties +exports.testTabContentTypeAndReload = function(assert, done) { + open().then(focus).then(function(window) { + let url = "data:text/html;charset=utf-8,foofoo"; + let urlXML = "data:text/xml;charset=utf-8,bar"; + tabs.open({ + url: url, + onReady: function(tab) { + if (tab.url === url) { + assert.equal(tab.contentType, "text/html"); + tab.url = urlXML; + } + else { + assert.equal(tab.contentType, "text/xml"); + close(window).then(done).then(null, assert.fail); + } + } + }); + }); +}; + +// TEST: tabs iterator and length property +exports.testTabsIteratorAndLength = function(assert, done) { + open(null, { features: { chrome: true, toolbar: true } }).then(focus).then(function(window) { + let startCount = 0; + for (let t of tabs) startCount++; + assert.equal(startCount, tabs.length, "length property is correct"); + let url = "data:text/html;charset=utf-8,default"; + + tabs.open(url); + tabs.open(url); + tabs.open({ + url: url, + onOpen: function(tab) { + let count = 0; + for (let t of tabs) count++; + assert.equal(count, startCount + 3, "iterated tab count matches"); + assert.equal(startCount + 3, tabs.length, "iterated tab count matches length property"); + + close(window).then(done).then(null, assert.fail); + } + }); + }); +}; + +// TEST: tab.url setter +exports.testTabLocation = function(assert, done) { + open().then(focus).then(function(window) { + let url1 = "data:text/html;charset=utf-8,foo"; + let url2 = "data:text/html;charset=utf-8,bar"; + + tabs.on('ready', function onReady(tab) { + if (tab.url != url2) + return; + tabs.removeListener('ready', onReady); + assert.pass("tab.load() loaded the correct url"); + close(window).then(done).then(null, assert.fail); + }); + + tabs.open({ + url: url1, + onOpen: function(tab) { + tab.url = url2 + } + }); + }); +}; + +// TEST: tab.close() +exports.testTabClose = function(assert, done) { + let testName = "testTabClose"; + let url = "data:text/html;charset=utf-8," + testName; + + assert.notEqual(tabs.activeTab.url, url, "tab is not the active tab"); + tabs.once('ready', function onReady(tab) { + assert.equal(tabs.activeTab.url, tab.url, "tab is now the active tab"); + assert.equal(url, tab.url, "tab url is the test url"); + let secondOnCloseCalled = false; + + // Bug 699450: Multiple calls to tab.close should not throw + tab.close(() => secondOnCloseCalled = true); + try { + tab.close(function () { + assert.notEqual(tabs.activeTab.url, url, "tab is no longer the active tab"); + assert.ok(secondOnCloseCalled, + "The immediate second call to tab.close happened"); + assert.notEqual(tabs.activeTab.url, url, "tab is no longer the active tab"); + + done(); + }); + } + catch(e) { + assert.fail("second call to tab.close() thrown an exception: " + e); + } + }); + + tabs.open(url); +}; + +// TEST: tab.move() +exports.testTabMove = function(assert, done) { + open().then(focus).then(function(window) { + let url = "data:text/html;charset=utf-8,foo"; + + tabs.open({ + url: url, + onOpen: function(tab) { + assert.equal(tab.index, 1, "tab index before move matches"); + tab.index = 0; + assert.equal(tab.index, 0, "tab index after move matches"); + close(window).then(done).then(null, assert.fail); + } + }); + }).then(null, assert.fail); +}; + +exports.testIgnoreClosing = function*(assert) { + let url = "data:text/html;charset=utf-8,foobar"; + let originalWindow = getMostRecentBrowserWindow(); + + let window = yield open().then(focus); + + assert.equal(tabs.length, 2, "should be two windows open each with one tab"); + + yield new Promise(resolve => { + tabs.once("ready", (tab) => { + let win = tab.window; + assert.equal(win.tabs.length, 2, "should be two tabs in the new window"); + assert.equal(tabs.length, 3, "should be three tabs in total"); + + tab.close(() => { + assert.equal(win.tabs.length, 1, "should be one tab in the new window"); + assert.equal(tabs.length, 2, "should be two tabs in total"); + resolve(); + }); + }); + + tabs.open(url); + }); +}; + +// TEST: open tab with default options +exports.testOpen = function(assert, done) { + let url = "data:text/html;charset=utf-8,default"; + tabs.open({ + url: url, + onReady: function(tab) { + assert.equal(tab.url, url, "URL of the new tab matches"); + assert.equal(tab.isPinned, false, "The new tab is not pinned"); + + tab.close(done); + } + }); +}; + +// TEST: opening a pinned tab +exports.testOpenPinned = function(assert, done) { + let url = "data:text/html;charset=utf-8,default"; + tabs.open({ + url: url, + isPinned: true, + onOpen: function(tab) { + assert.equal(tab.isPinned, true, "The new tab is pinned"); + tab.close(done); + } + }); +}; + +// TEST: pin/unpin opened tab +exports.testPinUnpin = function(assert, done) { + let url = "data:text/html;charset=utf-8,default"; + tabs.open({ + url: url, + inBackground: true, + onOpen: function(tab) { + tab.pin(); + assert.equal(tab.isPinned, true, "The tab was pinned correctly"); + tab.unpin(); + assert.equal(tab.isPinned, false, "The tab was unpinned correctly"); + tab.close(done); + } + }); +} + +// TEST: open tab in background +exports.testInBackground = function(assert, done) { + assert.equal(tabs.length, 1, "Should be one tab"); + + let window = getMostRecentBrowserWindow(); + let activeUrl = tabs.activeTab.url; + let url = "data:text/html;charset=utf-8,background"; + assert.equal(getMostRecentBrowserWindow(), window, "getMostRecentBrowserWindow() matches this window"); + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + assert.equal(tabs.activeTab.url, activeUrl, "URL of active tab has not changed"); + assert.equal(tab.url, url, "URL of the new background tab matches"); + assert.equal(getMostRecentBrowserWindow(), window, "a new window was not opened"); + assert.notEqual(tabs.activeTab.url, url, "URL of active tab is not the new URL"); + tab.close(done); + }); + + tabs.open({ + url: url, + inBackground: true + }); +} + +// TEST: open tab in new window +exports.testOpenInNewWindow = function(assert, done) { + let startWindowCount = windows().length; + + let url = "data:text/html;charset=utf-8,testOpenInNewWindow"; + tabs.open({ + url: url, + inNewWindow: true, + onReady: function(tab) { + let newWindow = getOwnerWindow(viewFor(tab)); + assert.equal(windows().length, startWindowCount + 1, "a new window was opened"); + + onFocus(newWindow).then(function() { + assert.equal(getMostRecentBrowserWindow(), newWindow, "new window is active"); + assert.equal(tab.url, url, "URL of the new tab matches"); + assert.equal(newWindow.content.location, url, "URL of new tab in new window matches"); + assert.equal(tabs.activeTab.url, url, "URL of activeTab matches"); + + return close(newWindow).then(done); + }).then(null, assert.fail); + } + }); + +} + +// Test tab.open inNewWindow + onOpen combination +exports.testOpenInNewWindowOnOpen = function(assert, done) { + let startWindowCount = windows().length; + + let url = "data:text/html;charset=utf-8,newwindow"; + tabs.open({ + url: url, + inNewWindow: true, + onOpen: function(tab) { + let newWindow = getOwnerWindow(viewFor(tab)); + + onFocus(newWindow).then(function() { + assert.equal(windows().length, startWindowCount + 1, "a new window was opened"); + assert.equal(getMostRecentBrowserWindow(), newWindow, "new window is active"); + + close(newWindow).then(done).then(null, assert.fail); + }); + } + }); +}; + +// TEST: onOpen event handler +exports.testTabsEvent_onOpen = function(assert, done) { + open().then(focus).then(window => { + let url = "data:text/html;charset=utf-8,1"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('open', listener1); + + // add listener via collection add + tabs.on('open', function listener2(tab) { + assert.equal(++eventCount, 2, "both listeners notified"); + tabs.removeListener('open', listener1); + tabs.removeListener('open', listener2); + close(window).then(done).then(null, assert.fail); + }); + + tabs.open(url); + }).then(null, assert.fail); +}; + +// TEST: onClose event handler +exports.testTabsEvent_onClose = function*(assert) { + let window = yield open().then(focus); + let url = "data:text/html;charset=utf-8,onclose"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + } + tabs.on("close", listener1); + + yield new Promise(resolve => { + // add listener via collection add + tabs.on("close", function listener2(tab) { + assert.equal(++eventCount, 2, "both listeners notified"); + tabs.removeListener("close", listener2); + resolve(); + }); + + tabs.on('ready', function onReady(tab) { + tabs.removeListener('ready', onReady); + tab.close(); + }); + + tabs.open(url); + }); + + tabs.removeListener("close", listener1); + assert.pass("done test!"); + + yield close(window); + assert.pass("window was closed!"); +}; + +// TEST: onClose event handler when a window is closed +exports.testTabsEvent_onCloseWindow = function(assert, done) { + let closeCount = 0; + let individualCloseCount = 0; + + open().then(focus).then(window => { + assert.pass('opened a new window'); + + tabs.on("close", function listener() { + if (++closeCount == 4) { + tabs.removeListener("close", listener); + } + }); + + function endTest() { + if (++individualCloseCount < 3) { + assert.pass('tab closed ' + individualCloseCount); + return; + } + + assert.equal(closeCount, 4, "Correct number of close events received"); + assert.equal(individualCloseCount, 3, + "Each tab with an attached onClose listener received a close " + + "event when the window was closed"); + + done(); + } + + // One tab is already open with the window + let openTabs = 1; + function testCasePossiblyLoaded() { + if (++openTabs == 4) { + window.close(); + } + assert.pass('tab opened ' + openTabs); + } + + tabs.open({ + url: "data:text/html;charset=utf-8,tab2", + onOpen: testCasePossiblyLoaded, + onClose: endTest + }); + + tabs.open({ + url: "data:text/html;charset=utf-8,tab3", + onOpen: testCasePossiblyLoaded, + onClose: endTest + }); + + tabs.open({ + url: "data:text/html;charset=utf-8,tab4", + onOpen: testCasePossiblyLoaded, + onClose: endTest + }); + }).then(null, assert.fail); +} + +// TEST: onReady event handler +exports.testTabsEvent_onReady = function(assert, done) { + open().then(focus).then(window => { + let url = "data:text/html;charset=utf-8,onready"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('ready', listener1); + + // add listener via collection add + tabs.on('ready', function listener2(tab) { + assert.equal(++eventCount, 2, "both listeners notified"); + tabs.removeListener('ready', listener1); + tabs.removeListener('ready', listener2); + close(window).then(done); + }); + + tabs.open(url); + }).then(null, assert.fail); +}; + +// TEST: onActivate event handler +exports.testTabsEvent_onActivate = function(assert, done) { + open().then(focus).then(window => { + let url = "data:text/html;charset=utf-8,onactivate"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + }; + tabs.on('activate', listener1); + + // add listener via collection add + tabs.on('activate', function listener2(tab) { + assert.equal(++eventCount, 2, "both listeners notified"); + tabs.removeListener('activate', listener1); + tabs.removeListener('activate', listener2); + close(window).then(done).then(null, assert.fail); + }); + + tabs.open(url); + }).then(null, assert.fail); +}; + +// onDeactivate event handler +exports.testTabsEvent_onDeactivate = function*(assert) { + let window = yield open().then(focus); + + let url = "data:text/html;charset=utf-8,ondeactivate"; + let eventCount = 0; + + // add listener via property assignment + function listener1(tab) { + eventCount++; + assert.pass("listener1 was called " + eventCount); + }; + tabs.on('deactivate', listener1); + + yield new Promise(resolve => { + // add listener via collection add + tabs.on('deactivate', function listener2(tab) { + assert.equal(++eventCount, 2, "both listeners notified"); + tabs.removeListener('deactivate', listener2); + resolve(); + }); + + tabs.on('open', function onOpen(tab) { + assert.pass("tab opened"); + tabs.removeListener('open', onOpen); + tabs.open("data:text/html;charset=utf-8,foo"); + }); + + tabs.open(url); + }); + + tabs.removeListener('deactivate', listener1); + assert.pass("listeners were removed"); +}; + +// pinning +exports.testTabsEvent_pinning = function(assert, done) { + open().then(focus).then(window => { + let url = "data:text/html;charset=utf-8,1"; + + tabs.on('open', function onOpen(tab) { + tabs.removeListener('open', onOpen); + tab.pin(); + }); + + tabs.on('pinned', function onPinned(tab) { + tabs.removeListener('pinned', onPinned); + assert.ok(tab.isPinned, "notified tab is pinned"); + tab.unpin(); + }); + + tabs.on('unpinned', function onUnpinned(tab) { + tabs.removeListener('unpinned', onUnpinned); + assert.ok(!tab.isPinned, "notified tab is not pinned"); + close(window).then(done).then(null, assert.fail); + }); + + tabs.open(url); + }).then(null, assert.fail); +}; + +// TEST: per-tab event handlers +exports.testPerTabEvents = function*(assert) { + let window = yield open().then(focus); + let eventCount = 0; + + let tab = yield new Promise(resolve => { + tabs.open({ + url: "data:text/html;charset=utf-8,foo", + onOpen: (tab) => { + assert.pass("the tab was opened"); + + // add listener via property assignment + function listener1() { + eventCount++; + }; + tab.on('ready', listener1); + + // add listener via collection add + tab.on('ready', function listener2() { + assert.equal(eventCount, 1, "listener1 called before listener2"); + tab.removeListener('ready', listener1); + tab.removeListener('ready', listener2); + assert.pass("removed listeners"); + eventCount++; + resolve(); + }); + } + }); + }); + + assert.equal(eventCount, 2, "both listeners were notified."); +}; + +exports.testAttachOnMultipleDocuments = function (assert, done) { + // Example of attach that process multiple tab documents + open().then(focus).then(window => { + let firstLocation = "data:text/html;charset=utf-8,foobar"; + let secondLocation = "data:text/html;charset=utf-8,bar"; + let thirdLocation = "data:text/html;charset=utf-8,fox"; + let onReadyCount = 0; + let worker1 = null; + let worker2 = null; + let detachEventCount = 0; + + tabs.open({ + url: firstLocation, + onReady: function (tab) { + onReadyCount++; + if (onReadyCount == 1) { + worker1 = tab.attach({ + contentScript: 'self.on("message", ' + + ' function () { return self.postMessage(document.location.href); }' + + ');', + onMessage: function (msg) { + assert.equal(msg, firstLocation, + "Worker url is equal to the 1st document"); + tab.url = secondLocation; + }, + onDetach: function () { + detachEventCount++; + assert.pass("Got worker1 detach event"); + assert.throws(function () { + worker1.postMessage("ex-1"); + }, + /Couldn't find the worker/, + "postMessage throw because worker1 is destroyed"); + checkEnd(); + } + }); + worker1.postMessage("new-doc-1"); + } + else if (onReadyCount == 2) { + + worker2 = tab.attach({ + contentScript: 'self.on("message", ' + + ' function () { return self.postMessage(document.location.href); }' + + ');', + onMessage: function (msg) { + assert.equal(msg, secondLocation, + "Worker url is equal to the 2nd document"); + tab.url = thirdLocation; + }, + onDetach: function () { + detachEventCount++; + assert.pass("Got worker2 detach event"); + assert.throws(function () { + worker2.postMessage("ex-2"); + }, + /Couldn't find the worker/, + "postMessage throw because worker2 is destroyed"); + checkEnd(); + } + }); + worker2.postMessage("new-doc-2"); + } + else if (onReadyCount == 3) { + tab.close(); + } + } + }); + + function checkEnd() { + if (detachEventCount != 2) + return; + + assert.pass("Got all detach events"); + + close(window).then(done).then(null, assert.fail); + } + }).then(null, assert.fail); +} + + +exports.testAttachWrappers = function (assert, done) { + // Check that content script has access to wrapped values by default + open().then(focus).then(window => { + let document = "data:text/html;charset=utf-8,"; + let count = 0; + + tabs.open({ + url: document, + onReady: function (tab) { + let worker = tab.attach({ + contentScript: 'try {' + + ' self.postMessage(!("globalJSVar" in window));' + + ' self.postMessage(typeof window.globalJSVar == "undefined");' + + '} catch(e) {' + + ' self.postMessage(e.message);' + + '}', + onMessage: function (msg) { + assert.equal(msg, true, "Worker has wrapped objects ("+count+")"); + if (count++ == 1) + close(window).then(done).then(null, assert.fail); + } + }); + } + }); + }).then(null, assert.fail); +} + +/* +// We do not offer unwrapped access to DOM since bug 601295 landed +// See 660780 to track progress of unwrap feature +exports.testAttachUnwrapped = function (assert, done) { + // Check that content script has access to unwrapped values through unsafeWindow + openBrowserWindow(function(window, browser) { + let document = "data:text/html;charset=utf-8,"; + let count = 0; + + tabs.open({ + url: document, + onReady: function (tab) { + let worker = tab.attach({ + contentScript: 'try {' + + ' self.postMessage(unsafeWindow.globalJSVar);' + + '} catch(e) {' + + ' self.postMessage(e.message);' + + '}', + onMessage: function (msg) { + assert.equal(msg, true, "Worker has access to javascript content globals ("+count+")"); + close(window).then(done); + } + }); + } + }); + + }); +} +*/ + +exports['test window focus changes active tab'] = function(assert, done) { + let url1 = "data:text/html;charset=utf-8," + encodeURIComponent("test window focus changes active tab

Window #1"); + + let win1 = openBrowserWindow(function() { + assert.pass("window 1 is open"); + + let win2 = openBrowserWindow(function() { + assert.pass("window 2 is open"); + + focus(win2).then(function() { + tabs.on("activate", function onActivate(tab) { + tabs.removeListener("activate", onActivate); + + if (tab.readyState === 'uninitialized') { + tab.once('ready', whenReady); + } + else { + whenReady(tab); + } + + function whenReady(tab) { + assert.pass("activate was called on windows focus change."); + assert.equal(tab.url, url1, 'the activated tab url is correct'); + + return close(win2).then(function() { + assert.pass('window 2 was closed'); + return close(win1); + }).then(done).then(null, assert.fail); + } + }); + + win1.focus(); + }); + }, "data:text/html;charset=utf-8,test window focus changes active tab

Window #2"); + }, url1); +}; + +exports['test ready event on new window tab'] = function(assert, done) { + let uri = encodeURI("data:text/html;charset=utf-8,Waiting for ready event!"); + + require("sdk/tabs").on("ready", function onReady(tab) { + if (tab.url === uri) { + require("sdk/tabs").removeListener("ready", onReady); + assert.pass("ready event was emitted"); + close(window).then(done).then(null, assert.fail); + } + }); + + let window = openBrowserWindow(function(){}, uri); +}; + +exports['test unique tab ids'] = function(assert, done) { + var windows = require('sdk/windows').browserWindows; + var { all, defer } = require('sdk/core/promise'); + + function openWindow() { + let deferred = defer(); + let win = windows.open({ + url: "data:text/html;charset=utf-8,foo", + }); + + win.on('open', function(window) { + assert.ok(window.tabs.length); + assert.ok(window.tabs.activeTab); + assert.ok(window.tabs.activeTab.id); + deferred.resolve({ + id: window.tabs.activeTab.id, + win: win + }); + }); + + return deferred.promise; + } + + var one = openWindow(), two = openWindow(); + all([one, two]).then(function(results) { + assert.notEqual(results[0].id, results[1].id, "tab Ids should not be equal."); + results[0].win.close(function() { + results[1].win.close(function () { + done(); + }); + }); + }); +} + +// related to Bug 671305 +exports.testOnLoadEventWithDOM = function(assert, done) { + let count = 0; + let title = 'testOnLoadEventWithDOM'; + + // open a about: url + tabs.open({ + url: 'data:text/html;charset=utf-8,' + title + '', + inBackground: true, + onLoad: function(tab) { + assert.equal(tab.title, title, 'tab passed in as arg, load called'); + + if (++count > 1) { + assert.pass('onLoad event called on reload'); + tab.close(done); + } + else { + assert.pass('first onLoad event occured'); + tab.reload(); + } + } + }); +}; + +// related to Bug 671305 +exports.testOnLoadEventWithImage = function(assert, done) { + let count = 0; + + tabs.open({ + url: base64jpeg, + inBackground: true, + onLoad: function(tab) { + if (++count > 1) { + assert.pass('onLoad event called on reload with image'); + tab.close(done); + } + else { + assert.pass('first onLoad event occured'); + tab.reload(); + } + } + }); +}; + +exports.testNoDeadObjects = function(assert, done) { + let loader = Loader(module); + let myTabs = loader.require("sdk/tabs"); + + // Load a tab, unload our modules, and navigate the tab to trigger an event + // on it. This would throw a dead object exception if our modules didn't + // clean up their event handlers on unload. + tabs.open({ + url: "data:text/html;charset=utf-8,one", + onLoad: function(tab) { + // 2. Arrange to nuke the sandboxes and then trigger the load event + // on the tab once the loader is kaput. + systemEvents.on("sdk:loader:destroy", function onUnload() { + systemEvents.off("sdk:loader:destroy", onUnload, true); + // Defer this carnage till the end of the event queue, to avoid nuking + // the sandboxes from under the modules as they're being cleaned up. + setTimeout(function() { + // 3. Arrange to close the tab once the second page loads. + tab.on("load", function() { + tab.close(function() { + let { viewFor } = loader.require("sdk/view/core"); + assert.equal(viewFor(tab), undefined, "didn't retain the closed tab"); + done(); + }); + }); + + // Trigger a load event on the tab, to give the now-unloaded + // myTabs a chance to choke on it. + tab.url = "data:text/html;charset=utf-8,two"; + }, 0); + }, true); + + // 1. Start unloading the modules. Defer till the end of the event + // queue, in case myTabs is attaching its own handlers here too. + // We want it to latch on before we pull the rug from under it. + setTimeout(function() { + loader.unload(); + }, 0); + } + }); +}; + +exports.testTabDestroy = function(assert, done) { + let loader = Loader(module); + let myTabs = loader.require("sdk/tabs"); + let { modelFor: myModelFor } = loader.require("sdk/model/core"); + let { viewFor: myViewFor } = loader.require("sdk/view/core"); + let myFirstTab = myTabs.activeTab; + + myTabs.open({ + url: "data:text/html;charset=utf-8,destroy", + onReady: (myTab) => setImmediate(() => { + let tab = modelFor(myViewFor(myTab)); + + function badListener(event, tab) { + // Ignore events for the other tabs + if (tab != myTab) + return; + assert.fail("Should not have seen the " + event + " listener called"); + } + + assert.ok(myTab, "Should have a tab in the test loader."); + assert.equal(myViewFor(myTab), viewFor(tab), "Should have the right view."); + assert.equal(myTabs.length, 2, "Should have the right number of global tabs."); + assert.equal(myTab.window.tabs.length, 2, "Should have the right number of window tabs."); + assert.equal(myTabs.activeTab, myTab, "Globally active tab is correct."); + assert.equal(myTab.window.tabs.activeTab, myTab, "Window active tab is correct."); + + assert.equal(myTabs[1], myTab, "Global tabs list contains tab."); + assert.equal(myTab.window.tabs[1], myTab, "Window tabs list contains tab."); + + myTab.once("ready", badListener.bind(null, "tab ready")); + myTab.once("deactivate", badListener.bind(null, "tab deactivate")); + myTab.once("activate", badListener.bind(null, "tab activate")); + myTab.once("close", badListener.bind(null, "tab close")); + + myTab.destroy(); + + myTab.once("ready", badListener.bind(null, "new tab ready")); + myTab.once("deactivate", badListener.bind(null, "new tab deactivate")); + myTab.once("activate", badListener.bind(null, "new tab activate")); + myTab.once("close", badListener.bind(null, "new tab close")); + + myTabs.once("ready", badListener.bind(null, "tabs ready")); + myTabs.once("deactivate", badListener.bind(null, "tabs deactivate")); + myTabs.once("activate", badListener.bind(null, "tabs activate")); + myTabs.once("close", badListener.bind(null, "tabs close")); + + assert.equal(myViewFor(myTab), viewFor(tab), "Should have the right view."); + assert.equal(myModelFor(viewFor(tab)), myTab, "Can still reach the tab object."); + assert.equal(myTabs.length, 2, "Should have the right number of global tabs."); + assert.equal(myTab.window.tabs.length, 2, "Should have the right number of window tabs."); + assert.equal(myTabs.activeTab, myTab, "Globally active tab is correct."); + assert.equal(myTab.window.tabs.activeTab, myTab, "Window active tab is correct."); + + assert.equal(myTabs[1], myTab, "Global tabs list still contains tab."); + assert.equal(myTab.window.tabs[1], myTab, "Window tabs list still contains tab."); + + assert.equal(myTab.url, undefined, "url property is not usable"); + assert.equal(myTab.contentType, undefined, "contentType property is not usable"); + assert.equal(myTab.title, undefined, "title property is not usable"); + assert.equal(myTab.id, undefined, "id property is not usable"); + assert.equal(myTab.index, undefined, "index property is not usable"); + + myTab.pin(); + assert.ok(!tab.isPinned, "pin method shouldn't work"); + + tabs.once("activate", () => setImmediate(() => { + assert.equal(myTabs.activeTab, myFirstTab, "Globally active tab is correct."); + assert.equal(myTab.window.tabs.activeTab, myFirstTab, "Window active tab is correct."); + + let sawActivate = false; + tabs.once("activate", () => setImmediate(() => { + sawActivate = true; + + assert.equal(myTabs.activeTab, myTab, "Globally active tab is correct."); + assert.equal(myTab.window.tabs.activeTab, myTab, "Window active tab is correct."); + + // This shouldn't have any effect + myTab.close(); + + tab.once("ready", () => setImmediate(() => { + tab.close(() => { + loader.unload(); + done(); + }); + })); + tab.url = "data:text/html;charset=utf-8,destroy2"; + })); + + myTab.activate(); + setImmediate(() => { + assert.ok(!sawActivate, "activate method shouldn't have done anything"); + + tab.activate(); + }); + })); + myFirstTab.activate(); + }) + }) +}; + +// related to bug 942511 +// https://bugzilla.mozilla.org/show_bug.cgi?id=942511 +exports['test active tab properties defined on popup closed'] = function (assert, done) { + setPref(OPEN_IN_NEW_WINDOW_PREF, 2); + setPref(DISABLE_POPUP_PREF, false); + + let tabID = ""; + let popupClosed = false; + + tabs.open({ + url: 'about:blank', + onReady: function (tab) { + tabID = tab.id; + tab.attach({ + contentScript: 'var popup = window.open("about:blank");' + + 'popup.close();' + }); + + windowObserver.once('close', () => { + popupClosed = true; + }); + + windowObserver.on('activate', () => { + // Only when the 'activate' event is fired after the popup was closed. + if (popupClosed) { + popupClosed = false; + let activeTabID = tabs.activeTab.id; + if (activeTabID) { + assert.equal(tabID, activeTabID, 'ActiveTab properties are correct'); + } + else { + assert.fail('ActiveTab properties undefined on popup closed'); + } + tab.close(done); + } + }); + } + }); +}; + +// related to bugs 922956 and 989288 +// https://bugzilla.mozilla.org/show_bug.cgi?id=922956 +// https://bugzilla.mozilla.org/show_bug.cgi?id=989288 +exports["test tabs ready and close after window.open"] = function*(assert, done) { + // ensure popups open in a new window and disable popup blocker + setPref(OPEN_IN_NEW_WINDOW_PREF, 2); + setPref(DISABLE_POPUP_PREF, false); + + // open windows to trigger observers + tabs.activeTab.attach({ + contentScript: "window.open('about:blank');" + + "window.open('about:blank', '', " + + "'width=800,height=600,resizable=no,status=no,location=no');" + }); + + let tab1 = yield wait(tabs, "ready"); + assert.pass("first tab ready has occured"); + + let tab2 = yield wait(tabs, "ready"); + assert.pass("second tab ready has occured"); + + tab1.close(); + yield wait(tabs, "close"); + assert.pass("first tab close has occured"); + + tab2.close(); + yield wait(tabs, "close"); + assert.pass("second tab close has occured"); +}; + +// related to bug #939496 +exports["test tab open event for new window"] = function(assert, done) { + // ensure popups open in a new window and disable popup blocker + setPref(OPEN_IN_NEW_WINDOW_PREF, 2); + setPref(DISABLE_POPUP_PREF, false); + + tabs.once('open', function onOpen(window) { + assert.pass("tab open has occured"); + window.close(done); + }); + + // open window to trigger observers + browserWindows.open("about:logo"); +}; + +after(exports, function*(name, assert) { + resetPopupPrefs(); + yield cleanUI(); +}); + +const resetPopupPrefs = () => { + resetPref(OPEN_IN_NEW_WINDOW_PREF); + resetPref(DISABLE_POPUP_PREF); +}; + +/******************* helpers *********************/ + +// Utility function to open a new browser window. +function openBrowserWindow(callback, url) { + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Ci.nsIWindowWatcher); + let urlString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + urlString.data = url; + let window = ww.openWindow(null, "chrome://browser/content/browser.xul", + "_blank", "chrome,all,dialog=no", urlString); + + if (callback) { + window.addEventListener("load", function onLoad(event) { + if (event.target && event.target.defaultView == window) { + window.removeEventListener("load", onLoad, true); + let browsers = window.document.getElementsByTagName("tabbrowser"); + try { + setTimeout(function () { + callback(window, browsers[0]); + }, 10); + } + catch (e) { + console.exception(e); + } + } + }, true); + } + + return window; +} + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/tabs/utils.js b/addon-sdk/source/test/tabs/utils.js new file mode 100644 index 000000000..4981a4d08 --- /dev/null +++ b/addon-sdk/source/test/tabs/utils.js @@ -0,0 +1,24 @@ +/* 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 { openTab: makeTab, getTabContentWindow } = require("sdk/tabs/utils"); + +function openTab(rawWindow, url) { + return new Promise(resolve => { + let tab = makeTab(rawWindow, url); + let window = getTabContentWindow(tab); + if (window.document.readyState == "complete") { + return resolve(); + } + + window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad, true); + resolve(); + }, true); + + return null; + }) +} +exports.openTab = openTab; diff --git a/addon-sdk/source/test/test-addon-bootstrap.js b/addon-sdk/source/test/test-addon-bootstrap.js new file mode 100644 index 000000000..01e2d15fc --- /dev/null +++ b/addon-sdk/source/test/test-addon-bootstrap.js @@ -0,0 +1,97 @@ +/* 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, Cc, Ci } = require("chrome"); +const { create, evaluate } = require("./fixtures/bootstrap/utils"); + +const ROOT = require.resolve("sdk/base64").replace("/sdk/base64.js", ""); + +// Note: much of this test code is from +// http://dxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm +const BOOTSTRAP_REASONS = { + APP_STARTUP : 1, + APP_SHUTDOWN : 2, + ADDON_ENABLE : 3, + ADDON_DISABLE : 4, + ADDON_INSTALL : 5, + ADDON_UNINSTALL : 6, + ADDON_UPGRADE : 7, + ADDON_DOWNGRADE : 8 +}; + +exports["test install/startup/shutdown/uninstall all return a promise"] = function(assert) { + let uri = require.resolve("./fixtures/addon/bootstrap.js"); + let id = "test-min-boot@jetpack"; + let bootstrapScope = create({ + id: id, + uri: uri + }); + + // As we don't want our caller to control the JS version used for the + // bootstrap file, we run loadSubScript within the context of the + // sandbox with the latest JS version set explicitly. + bootstrapScope.ROOT = ROOT; + + evaluate({ + uri: uri, + scope: bootstrapScope + }); + + let addon = { + id: id, + version: "0.0.1", + resourceURI: { + spec: uri.replace("bootstrap.js", "") + } + }; + + let install = bootstrapScope.install(addon, BOOTSTRAP_REASONS.ADDON_INSTALL); + yield install.then(() => assert.pass("install returns a promise")); + + let startup = bootstrapScope.startup(addon, BOOTSTRAP_REASONS.ADDON_INSTALL); + yield startup.then(() => assert.pass("startup returns a promise")); + + let shutdown = bootstrapScope.shutdown(addon, BOOTSTRAP_REASONS.ADDON_DISABLE); + yield shutdown.then(() => assert.pass("shutdown returns a promise")); + + // calling shutdown multiple times is fine + shutdown = bootstrapScope.shutdown(addon, BOOTSTRAP_REASONS.ADDON_DISABLE); + yield shutdown.then(() => assert.pass("shutdown returns working promise on multiple calls")); + + let uninstall = bootstrapScope.uninstall(addon, BOOTSTRAP_REASONS.ADDON_UNINSTALL); + yield uninstall.then(() => assert.pass("uninstall returns a promise")); +} + +exports["test minimal bootstrap.js"] = function*(assert) { + let uri = require.resolve("./fixtures/addon/bootstrap.js"); + let bootstrapScope = create({ + id: "test-min-boot@jetpack", + uri: uri + }); + + // As we don't want our caller to control the JS version used for the + // bootstrap file, we run loadSubScript within the context of the + // sandbox with the latest JS version set explicitly. + bootstrapScope.ROOT = ROOT; + + assert.equal(typeof bootstrapScope.install, "undefined", "install DNE"); + assert.equal(typeof bootstrapScope.startup, "undefined", "startup DNE"); + assert.equal(typeof bootstrapScope.shutdown, "undefined", "shutdown DNE"); + assert.equal(typeof bootstrapScope.uninstall, "undefined", "uninstall DNE"); + + evaluate({ + uri: uri, + scope: bootstrapScope + }); + + assert.equal(typeof bootstrapScope.install, "function", "install exists"); + assert.equal(typeof bootstrapScope.startup, "function", "startup exists"); + assert.equal(typeof bootstrapScope.shutdown, "function", "shutdown exists"); + assert.equal(typeof bootstrapScope.uninstall, "function", "uninstall exists"); + + bootstrapScope.shutdown(null, BOOTSTRAP_REASONS.ADDON_DISABLE); +} + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-addon-extras.js b/addon-sdk/source/test/test-addon-extras.js new file mode 100644 index 000000000..1910db05e --- /dev/null +++ b/addon-sdk/source/test/test-addon-extras.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"; + +const { Ci, Cu, Cc, components } = require("chrome"); +const self = require("sdk/self"); +const { before, after } = require("sdk/test/utils"); +const fixtures = require("./fixtures"); +const { Loader } = require("sdk/test/loader"); +const { merge } = require("sdk/util/object"); + +exports["test changing result from addon extras in panel"] = function(assert, done) { + let loader = Loader(module, null, null, { + modules: { + "sdk/self": merge({}, self, { + data: merge({}, self.data, {url: fixtures.url}) + }) + } + }); + + const { Panel } = loader.require("sdk/panel"); + const { events } = loader.require("sdk/content/sandbox/events"); + const { on } = loader.require("sdk/event/core"); + const { isAddonContent } = loader.require("sdk/content/utils"); + + var result = 1; + var extrasVal = { + test: function() { + return result; + } + }; + + on(events, "content-script-before-inserted", ({ window, worker }) => { + assert.pass("content-script-before-inserted"); + + if (isAddonContent({ contentURL: window.location.href })) { + let extraStuff = Cu.cloneInto(extrasVal, window, { + cloneFunctions: true + }); + getUnsafeWindow(window).extras = extraStuff; + + assert.pass("content-script-before-inserted done!"); + } + }); + + let panel = Panel({ + contentURL: "./test-addon-extras.html" + }); + + panel.port.once("result1", (result) => { + assert.equal(result, 1, "result is a number"); + result = true; + panel.port.emit("get-result"); + }); + + panel.port.once("result2", (result) => { + assert.equal(result, true, "result is a boolean"); + loader.unload(); + done(); + }); + + panel.port.emit("get-result"); +} + +function getUnsafeWindow (win) { + return win.wrappedJSObject || win; +} + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-addon-installer.js b/addon-sdk/source/test/test-addon-installer.js new file mode 100644 index 000000000..bb39cca2d --- /dev/null +++ b/addon-sdk/source/test/test-addon-installer.js @@ -0,0 +1,230 @@ +/* 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, Ci, Cu } = require("chrome"); +const { pathFor } = require("sdk/system"); +const AddonInstaller = require("sdk/addon/installer"); +const { on, off } = require("sdk/system/events"); +const { setTimeout } = require("sdk/timers"); +const fs = require("sdk/io/fs"); +const path = require("sdk/fs/path"); +const { OS } = require("resource://gre/modules/osfile.jsm"); +const { toFilename } = require("sdk/url"); + +// Retrieve the path to the OS temporary directory: +const tmpDir = pathFor("TmpD"); + +const profilePath = pathFor("ProfD"); +const corruptXPIPath = path.join(profilePath, "sdk-corrupt.xpi"); +const testFolderURL = module.uri.split('test-addon-installer.js')[0]; +const ADDON_URL = toFilename(testFolderURL + "fixtures/addon-install-unit-test@mozilla.com.xpi"); + +exports["test Install"] = function*(assert) { + var ADDON_PATH = OS.Path.join(OS.Constants.Path.tmpDir, "install-test.xpi"); + + assert.pass("Copying test add-on " + ADDON_URL + " to " + ADDON_PATH); + + yield OS.File.copy(ADDON_URL, ADDON_PATH); + + assert.pass("Copied test add-on to " + ADDON_PATH); + + // Save all events distpatched by bootstrap.js of the installed addon + let events = []; + function eventsObserver({ data }) { + events.push(data); + } + on("addon-install-unit-test", eventsObserver); + + // Install the test addon + yield AddonInstaller.install(ADDON_PATH).then((id) => { + assert.equal(id, "addon-install-unit-test@mozilla.com", "`id` is valid"); + + // Now uninstall it + return AddonInstaller.uninstall(id).then(function () { + // Ensure that bootstrap.js methods of the addon have been called + // successfully and in the right order + let expectedEvents = ["install", "startup", "shutdown", "uninstall"]; + assert.equal(JSON.stringify(events), + JSON.stringify(expectedEvents), + "addon's bootstrap.js functions have been called"); + + off("addon-install-unit-test", eventsObserver); + }); + }, (code) => { + assert.fail("Install failed: "+code); + off("addon-install-unit-test", eventsObserver); + }); + + assert.pass("Add-on was uninstalled."); + + yield OS.File.remove(ADDON_PATH); + + assert.pass("Removed the temp file"); +}; + +exports["test Failing Install With Invalid Path"] = function (assert, done) { + AddonInstaller.install("invalid-path").then( + function onInstalled(id) { + assert.fail("Unexpected success"); + done(); + }, + function onFailure(code) { + assert.equal(code, AddonInstaller.ERROR_FILE_ACCESS, + "Got expected error code"); + done(); + } + ); +}; + +exports["test Failing Install With Invalid File"] = function (assert, done) { + const content = "bad xpi"; + const path = corruptXPIPath; + + fs.writeFile(path, content, (error) => { + assert.equal(fs.readFileSync(path).toString(), + content, + "contet was written"); + + AddonInstaller.install(path).then( + () => { + assert.fail("Unexpected success"); + fs.unlink(path, done); + }, + (code) => { + assert.equal(code, AddonInstaller.ERROR_CORRUPT_FILE, + "Got expected error code"); + fs.unlink(path, done); + } + ); + }); +} + +exports["test Update"] = function*(assert) { + var ADDON_PATH = OS.Path.join(OS.Constants.Path.tmpDir, "update-test.xpi"); + + assert.pass("Copying test add-on " + ADDON_URL + " to " + ADDON_PATH); + + yield OS.File.copy(ADDON_URL, ADDON_PATH); + + assert.pass("Copied test add-on to " + ADDON_PATH); + + // Save all events distpatched by bootstrap.js of the installed addon + let events = []; + let iteration = 1; + let eventsObserver = ({data}) => events.push(data); + on("addon-install-unit-test", eventsObserver); + + yield new Promise(resolve => { + function onInstalled(id) { + let prefix = "[" + iteration + "] "; + assert.equal(id, "addon-install-unit-test@mozilla.com", + prefix + "`id` is valid"); + + // On 2nd and 3rd iteration, we receive uninstall events from the last + // previously installed addon + let expectedEvents = + iteration == 1 + ? ["install", "startup"] + : ["shutdown", "uninstall", "install", "startup"]; + assert.equal(JSON.stringify(events), + JSON.stringify(expectedEvents), + prefix + "addon's bootstrap.js functions have been called"); + + if (iteration++ < 3) { + next(); + } + else { + events = []; + AddonInstaller.uninstall(id).then(function() { + let expectedEvents = ["shutdown", "uninstall"]; + assert.equal(JSON.stringify(events), + JSON.stringify(expectedEvents), + prefix + "addon's bootstrap.js functions have been called"); + + off("addon-install-unit-test", eventsObserver); + resolve(); + }); + } + } + function onFailure(code) { + assert.fail("Install failed: "+code); + off("addon-install-unit-test", eventsObserver); + resolve(); + } + + function next() { + events = []; + AddonInstaller.install(ADDON_PATH).then(onInstalled, onFailure); + } + + next(); + }); + + assert.pass("Add-on was uninstalled."); + + yield OS.File.remove(ADDON_PATH); + + assert.pass("Removed the temp file"); +}; + +exports['test Uninstall failure'] = function (assert, done) { + AddonInstaller.uninstall('invalid-addon-path').then( + () => assert.fail('Addon uninstall should not resolve successfully'), + () => assert.pass('Addon correctly rejected invalid uninstall') + ).then(done, assert.fail); +}; + +exports['test Addon Disable and Enable'] = function*(assert) { + var ADDON_PATH = OS.Path.join(OS.Constants.Path.tmpDir, "disable-enable-test.xpi"); + + assert.pass("Copying test add-on " + ADDON_URL + " to " + ADDON_PATH); + + yield OS.File.copy(ADDON_URL, ADDON_PATH); + + assert.pass("Copied test add-on to " + ADDON_PATH); + + let ensureActive = (addonId) => AddonInstaller.isActive(addonId).then(state => { + assert.equal(state, true, 'Addon should be enabled by default'); + return addonId; + }); + let ensureInactive = (addonId) => AddonInstaller.isActive(addonId).then(state => { + assert.equal(state, false, 'Addon should be disabled after disabling'); + return addonId; + }); + + yield AddonInstaller.install(ADDON_PATH) + .then(ensureActive) + .then(AddonInstaller.enable) // should do nothing, yet not fail + .then(ensureActive) + .then(AddonInstaller.disable) + .then(ensureInactive) + .then(AddonInstaller.disable) // should do nothing, yet not fail + .then(ensureInactive) + .then(AddonInstaller.enable) + .then(ensureActive) + .then(AddonInstaller.uninstall); + + assert.pass("Add-on was uninstalled."); + + yield OS.File.remove(ADDON_PATH); + + assert.pass("Removed the temp file"); +}; + +exports['test Disable failure'] = function (assert, done) { + AddonInstaller.disable('not-an-id').then( + () => assert.fail('Addon disable should not resolve successfully'), + () => assert.pass('Addon correctly rejected invalid disable') + ).then(done, assert.fail); +}; + +exports['test Enable failure'] = function (assert, done) { + AddonInstaller.enable('not-an-id').then( + () => assert.fail('Addon enable should not resolve successfully'), + () => assert.pass('Addon correctly rejected invalid enable') + ).then(done, assert.fail); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-addon-window.js b/addon-sdk/source/test/test-addon-window.js new file mode 100644 index 000000000..8cb07bb07 --- /dev/null +++ b/addon-sdk/source/test/test-addon-window.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'; + +var { Loader } = require('sdk/test/loader'); + +exports.testReady = function(assert, done) { + let loader = Loader(module); + let { ready, window } = loader.require('sdk/addon/window'); + let windowIsReady = false; + + ready.then(function() { + assert.equal(windowIsReady, false, 'ready promise was resolved only once'); + windowIsReady = true; + + loader.unload(); + done(); + }).then(null, assert.fail); +} + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-api-utils.js b/addon-sdk/source/test/test-api-utils.js new file mode 100644 index 000000000..12f2bf44f --- /dev/null +++ b/addon-sdk/source/test/test-api-utils.js @@ -0,0 +1,316 @@ +/* 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 apiUtils = require("sdk/deprecated/api-utils"); + +exports.testValidateOptionsEmpty = function (assert) { + let val = apiUtils.validateOptions(null, {}); + + assert.deepEqual(val, {}); + + val = apiUtils.validateOptions(null, { foo: {} }); + assert.deepEqual(val, {}); + + val = apiUtils.validateOptions({}, {}); + assert.deepEqual(val, {}); + + val = apiUtils.validateOptions({}, { foo: {} }); + assert.deepEqual(val, {}); +}; + +exports.testValidateOptionsNonempty = function (assert) { + let val = apiUtils.validateOptions({ foo: 123 }, {}); + assert.deepEqual(val, {}); + + val = apiUtils.validateOptions({ foo: 123, bar: 456 }, + { foo: {}, bar: {}, baz: {} }); + + assert.deepEqual(val, { foo: 123, bar: 456 }); +}; + +exports.testValidateOptionsMap = function (assert) { + let val = apiUtils.validateOptions({ foo: 3, bar: 2 }, { + foo: { map: v => v * v }, + bar: { map: v => undefined } + }); + assert.deepEqual(val, { foo: 9, bar: undefined }); +}; + +exports.testValidateOptionsMapException = function (assert) { + let val = apiUtils.validateOptions({ foo: 3 }, { + foo: { map: function () { throw new Error(); }} + }); + assert.deepEqual(val, { foo: 3 }); +}; + +exports.testValidateOptionsOk = function (assert) { + let val = apiUtils.validateOptions({ foo: 3, bar: 2, baz: 1 }, { + foo: { ok: v => v }, + bar: { ok: v => v } + }); + assert.deepEqual(val, { foo: 3, bar: 2 }); + + assert.throws( + () => apiUtils.validateOptions({ foo: 2, bar: 2 }, { + bar: { ok: v => v > 2 } + }), + /^The option "bar" is invalid/, + "ok should raise exception on invalid option" + ); + + assert.throws( + () => apiUtils.validateOptions(null, { foo: { ok: v => v }}), + /^The option "foo" is invalid/, + "ok should raise exception on invalid option" + ); +}; + +exports.testValidateOptionsIs = function (assert) { + let opts = { + array: [], + boolean: true, + func: function () {}, + nul: null, + number: 1337, + object: {}, + string: "foo", + undef1: undefined + }; + let requirements = { + array: { is: ["array"] }, + boolean: { is: ["boolean"] }, + func: { is: ["function"] }, + nul: { is: ["null"] }, + number: { is: ["number"] }, + object: { is: ["object"] }, + string: { is: ["string"] }, + undef1: { is: ["undefined"] }, + undef2: { is: ["undefined"] } + }; + let val = apiUtils.validateOptions(opts, requirements); + assert.deepEqual(val, opts); + + assert.throws( + () => apiUtils.validateOptions(null, { + foo: { is: ["object", "number"] } + }), + /^The option "foo" must be one of the following types: object, number/, + "Invalid type should raise exception" + ); +}; + +exports.testValidateOptionsIsWithExportedValue = function (assert) { + let { string, number, boolean, object } = apiUtils; + + let opts = { + boolean: true, + number: 1337, + object: {}, + string: "foo" + }; + let requirements = { + string: { is: string }, + number: { is: number }, + boolean: { is: boolean }, + object: { is: object } + }; + let val = apiUtils.validateOptions(opts, requirements); + assert.deepEqual(val, opts); + + // Test the types are optional by default + val = apiUtils.validateOptions({foo: 'bar'}, requirements); + assert.deepEqual(val, {}); +}; + +exports.testValidateOptionsIsWithEither = function (assert) { + let { string, number, boolean, either } = apiUtils; + let text = { is: either(string, number) }; + + let requirements = { + text: text, + boolOrText: { is: either(text, boolean) } + }; + + let val = apiUtils.validateOptions({text: 12}, requirements); + assert.deepEqual(val, {text: 12}); + + val = apiUtils.validateOptions({text: "12"}, requirements); + assert.deepEqual(val, {text: "12"}); + + val = apiUtils.validateOptions({boolOrText: true}, requirements); + assert.deepEqual(val, {boolOrText: true}); + + val = apiUtils.validateOptions({boolOrText: "true"}, requirements); + assert.deepEqual(val, {boolOrText: "true"}); + + val = apiUtils.validateOptions({boolOrText: 1}, requirements); + assert.deepEqual(val, {boolOrText: 1}); + + assert.throws( + () => apiUtils.validateOptions({text: true}, requirements), + /^The option "text" must be one of the following types/, + "Invalid type should raise exception" + ); + + assert.throws( + () => apiUtils.validateOptions({boolOrText: []}, requirements), + /^The option "boolOrText" must be one of the following types/, + "Invalid type should raise exception" + ); +}; + +exports.testValidateOptionsWithRequiredAndOptional = function (assert) { + let { string, number, required, optional } = apiUtils; + + let opts = { + number: 1337, + string: "foo" + }; + + let requirements = { + string: required(string), + number: number + }; + + let val = apiUtils.validateOptions(opts, requirements); + assert.deepEqual(val, opts); + + val = apiUtils.validateOptions({string: "foo"}, requirements); + assert.deepEqual(val, {string: "foo"}); + + assert.throws( + () => apiUtils.validateOptions({number: 10}, requirements), + /^The option "string" must be one of the following types/, + "Invalid type should raise exception" + ); + + // Makes string optional + requirements.string = optional(requirements.string); + + val = apiUtils.validateOptions({number: 10}, requirements), + assert.deepEqual(val, {number: 10}); + +}; + + + +exports.testValidateOptionsWithExportedValue = function (assert) { + let { string, number, boolean, object } = apiUtils; + + let opts = { + boolean: true, + number: 1337, + object: {}, + string: "foo" + }; + let requirements = { + string: string, + number: number, + boolean: boolean, + object: object + }; + let val = apiUtils.validateOptions(opts, requirements); + assert.deepEqual(val, opts); + + // Test the types are optional by default + val = apiUtils.validateOptions({foo: 'bar'}, requirements); + assert.deepEqual(val, {}); +}; + + +exports.testValidateOptionsMapIsOk = function (assert) { + let [map, is, ok] = [false, false, false]; + let val = apiUtils.validateOptions({ foo: 1337 }, { + foo: { + map: v => v.toString(), + is: ["string"], + ok: v => v.length > 0 + } + }); + assert.deepEqual(val, { foo: "1337" }); + + let requirements = { + foo: { + is: ["object"], + ok: () => assert.fail("is should have caused us to throw by now") + } + }; + assert.throws( + () => apiUtils.validateOptions(null, requirements), + /^The option "foo" must be one of the following types: object/, + "is should be used before ok is called" + ); +}; + +exports.testValidateOptionsErrorMsg = function (assert) { + assert.throws( + () => apiUtils.validateOptions(null, { + foo: { ok: v => v, msg: "foo!" } + }), + /^foo!/, + "ok should raise exception with customized message" + ); +}; + +exports.testValidateMapWithMissingKey = function (assert) { + let val = apiUtils.validateOptions({ }, { + foo: { + map: v => v || "bar" + } + }); + assert.deepEqual(val, { foo: "bar" }); + + val = apiUtils.validateOptions({ }, { + foo: { + map: v => { throw "bar" } + } + }); + assert.deepEqual(val, { }); +}; + +exports.testValidateMapWithMissingKeyAndThrown = function (assert) { + let val = apiUtils.validateOptions({}, { + bar: { + map: function(v) { throw "bar" } + }, + baz: { + map: v => "foo" + } + }); + assert.deepEqual(val, { baz: "foo" }); +}; + +exports.testAddIterator = function testAddIterator (assert) { + let obj = {}; + let keys = ["foo", "bar", "baz"]; + let vals = [1, 2, 3]; + let keysVals = [["foo", 1], ["bar", 2], ["baz", 3]]; + apiUtils.addIterator( + obj, + function keysValsGen() { + for (let keyVal of keysVals) + yield keyVal; + } + ); + + let keysItr = []; + for (let key in obj) + keysItr.push(key); + + assert.equal(keysItr.length, keys.length, + "the keys iterator returns the correct number of items"); + for (let i = 0; i < keys.length; i++) + assert.equal(keysItr[i], keys[i], "the key is correct"); + + let valsItr = []; + for each (let val in obj) + valsItr.push(val); + assert.equal(valsItr.length, vals.length, + "the vals iterator returns the correct number of items"); + for (let i = 0; i < vals.length; i++) + assert.equal(valsItr[i], vals[i], "the val is correct"); + +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-array.js b/addon-sdk/source/test/test-array.js new file mode 100644 index 000000000..161d8033d --- /dev/null +++ b/addon-sdk/source/test/test-array.js @@ -0,0 +1,103 @@ +/* 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 array = require('sdk/util/array'); + +exports.testHas = function(assert) { + var testAry = [1, 2, 3]; + assert.equal(array.has([1, 2, 3], 1), true); + assert.equal(testAry.length, 3); + assert.equal(testAry[0], 1); + assert.equal(testAry[1], 2); + assert.equal(testAry[2], 3); + assert.equal(array.has(testAry, 2), true); + assert.equal(array.has(testAry, 3), true); + assert.equal(array.has(testAry, 4), false); + assert.equal(array.has(testAry, '1'), false); +}; +exports.testHasAny = function(assert) { + var testAry = [1, 2, 3]; + assert.equal(array.hasAny([1, 2, 3], [1]), true); + assert.equal(array.hasAny([1, 2, 3], [1, 5]), true); + assert.equal(array.hasAny([1, 2, 3], [5, 1]), true); + assert.equal(array.hasAny([1, 2, 3], [5, 2]), true); + assert.equal(array.hasAny([1, 2, 3], [5, 3]), true); + assert.equal(array.hasAny([1, 2, 3], [5, 4]), false); + assert.equal(testAry.length, 3); + assert.equal(testAry[0], 1); + assert.equal(testAry[1], 2); + assert.equal(testAry[2], 3); + assert.equal(array.hasAny(testAry, [2]), true); + assert.equal(array.hasAny(testAry, [3]), true); + assert.equal(array.hasAny(testAry, [4]), false); + assert.equal(array.hasAny(testAry), false); + assert.equal(array.hasAny(testAry, '1'), false); +}; + +exports.testAdd = function(assert) { + var testAry = [1]; + assert.equal(array.add(testAry, 1), false); + assert.equal(testAry.length, 1); + assert.equal(testAry[0], 1); + assert.equal(array.add(testAry, 2), true); + assert.equal(testAry.length, 2); + assert.equal(testAry[0], 1); + assert.equal(testAry[1], 2); +}; + +exports.testRemove = function(assert) { + var testAry = [1, 2]; + assert.equal(array.remove(testAry, 3), false); + assert.equal(testAry.length, 2); + assert.equal(testAry[0], 1); + assert.equal(testAry[1], 2); + assert.equal(array.remove(testAry, 2), true); + assert.equal(testAry.length, 1); + assert.equal(testAry[0], 1); +}; + +exports.testFlatten = function(assert) { + assert.equal(array.flatten([1, 2, 3]).length, 3); + assert.equal(array.flatten([1, [2, 3]]).length, 3); + assert.equal(array.flatten([1, [2, [3]]]).length, 3); + assert.equal(array.flatten([[1], [[2, [3]]]]).length, 3); +}; + +exports.testUnique = function(assert) { + var Class = function () {}; + var A = {}; + var B = new Class(); + var C = [ 1, 2, 3 ]; + var D = {}; + var E = new Class(); + + assert.deepEqual(array.unique([1,2,3,1,2]), [1,2,3]); + assert.deepEqual(array.unique([1,1,1,4,9,5,5]), [1,4,9,5]); + assert.deepEqual(array.unique([A, A, A, B, B, D]), [A,B,D]); + assert.deepEqual(array.unique([A, D, A, E, E, D, A, A, C]), [A, D, E, C]) +}; + +exports.testUnion = function(assert) { + var Class = function () {}; + var A = {}; + var B = new Class(); + var C = [ 1, 2, 3 ]; + var D = {}; + var E = new Class(); + + assert.deepEqual(array.union([1, 2, 3],[7, 1, 2]), [1, 2, 3, 7]); + assert.deepEqual(array.union([1, 1, 1, 4, 9, 5, 5], [10, 1, 5]), [1, 4, 9, 5, 10]); + assert.deepEqual(array.union([A, B], [A, D]), [A, B, D]); + assert.deepEqual(array.union([A, D], [A, E], [E, D, A], [A, C]), [A, D, E, C]); +}; + +exports.testFind = function(assert) { + let isOdd = (x) => x % 2; + assert.equal(array.find([2, 4, 5, 7, 8, 9], isOdd), 5); + assert.equal(array.find([2, 4, 6, 8], isOdd), undefined); + assert.equal(array.find([2, 4, 6, 8], isOdd, null), null); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-base64.js b/addon-sdk/source/test/test-base64.js new file mode 100644 index 000000000..b969413f9 --- /dev/null +++ b/addon-sdk/source/test/test-base64.js @@ -0,0 +1,100 @@ +/* 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 base64 = require("sdk/base64"); + +const text = "Awesome!"; +const b64text = "QXdlc29tZSE="; + +const utf8text = "\u2713 à la mode"; +const badutf8text = "\u0013 à la mode"; +const b64utf8text = "4pyTIMOgIGxhIG1vZGU="; + +// 1 MB string +const longtext = 'fff'.repeat(333333); +const b64longtext = 'ZmZm'.repeat(333333); + +exports["test base64.encode"] = function (assert) { + assert.equal(base64.encode(text), b64text, "encode correctly") +} + +exports["test base64.decode"] = function (assert) { + assert.equal(base64.decode(b64text), text, "decode correctly") +} + +exports["test base64.encode Unicode"] = function (assert) { + + assert.equal(base64.encode(utf8text, "utf-8"), b64utf8text, + "encode correctly Unicode strings.") +} + +exports["test base64.decode Unicode"] = function (assert) { + + assert.equal(base64.decode(b64utf8text, "utf-8"), utf8text, + "decode correctly Unicode strings.") +} + +exports["test base64.encode long string"] = function (assert) { + + assert.equal(base64.encode(longtext), b64longtext, "encode long strings") +} + +exports["test base64.decode long string"] = function (assert) { + + assert.equal(base64.decode(b64longtext), longtext, "decode long strings") +} + +exports["test base64.encode treats input as octet string"] = function (assert) { + + assert.equal(base64.encode("\u0066"), "Zg==", + "treat octet string as octet string") + assert.equal(base64.encode("\u0166"), "Zg==", + "treat non-octet string as octet string") + assert.equal(base64.encode("\uff66"), "Zg==", + "encode non-octet string as octet string") +} + +exports["test base64.encode with wrong charset"] = function (assert) { + + assert.throws(function() { + base64.encode(utf8text, "utf-16"); + }, "The charset argument can be only 'utf-8'"); + + assert.throws(function() { + base64.encode(utf8text, ""); + }, "The charset argument can be only 'utf-8'"); + + assert.throws(function() { + base64.encode(utf8text, 8); + }, "The charset argument can be only 'utf-8'"); + +} + +exports["test base64.decode with wrong charset"] = function (assert) { + + assert.throws(function() { + base64.decode(utf8text, "utf-16"); + }, "The charset argument can be only 'utf-8'"); + + assert.throws(function() { + base64.decode(utf8text, ""); + }, "The charset argument can be only 'utf-8'"); + + assert.throws(function() { + base64.decode(utf8text, 8); + }, "The charset argument can be only 'utf-8'"); + +} + +exports["test encode/decode Unicode without utf-8 as charset"] = function (assert) { + + assert.equal(base64.decode(base64.encode(utf8text)), badutf8text, + "Unicode strings needs 'utf-8' charset or will be mangled" + ); + +} + +require("test").run(exports); diff --git a/addon-sdk/source/test/test-bootstrap.js b/addon-sdk/source/test/test-bootstrap.js new file mode 100644 index 000000000..52a713e61 --- /dev/null +++ b/addon-sdk/source/test/test-bootstrap.js @@ -0,0 +1,19 @@ +/* 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 { Request } = require("sdk/request"); + +exports.testBootstrapExists = function (assert, done) { + Request({ + url: "resource://gre/modules/sdk/bootstrap.js", + onComplete: function (response) { + if (response.text) + assert.pass("the bootstrap file was found"); + done(); + } + }).get(); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-browser-events.js b/addon-sdk/source/test/test-browser-events.js new file mode 100644 index 000000000..9402f1ec5 --- /dev/null +++ b/addon-sdk/source/test/test-browser-events.js @@ -0,0 +1,102 @@ +/* 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 = { + engines: { + "Firefox": "*" + } +}; + +const { Loader } = require("sdk/test/loader"); +const { open, getMostRecentBrowserWindow, getOuterId } = require("sdk/window/utils"); +const { setTimeout } = require("sdk/timers"); + +exports["test browser events"] = function(assert, done) { + let loader = Loader(module); + let { events } = loader.require("sdk/browser/events"); + let { on, off } = loader.require("sdk/event/core"); + let actual = []; + + on(events, "data", function handler(e) { + actual.push(e); + if (e.type === "load") window.close(); + if (e.type === "close") { + // Unload the module so that all listeners set by observer are removed. + + let [ ready, load, close ] = actual; + + assert.equal(ready.type, "DOMContentLoaded"); + assert.equal(ready.target, window, "window ready"); + + assert.equal(load.type, "load"); + assert.equal(load.target, window, "window load"); + + assert.equal(close.type, "close"); + assert.equal(close.target, window, "window load"); + + // Note: If window is closed right after this GC won't have time + // to claim loader and there for this listener, there for it's safer + // to remove listener. + off(events, "data", handler); + loader.unload(); + done(); + } + }); + + // Open window and close it to trigger observers. + let window = open(); +}; + +exports["test browser events ignore other wins"] = function(assert, done) { + let loader = Loader(module); + let { events: windowEvents } = loader.require("sdk/window/events"); + let { events: browserEvents } = loader.require("sdk/browser/events"); + let { on, off } = loader.require("sdk/event/core"); + let actualBrowser = []; + let actualWindow = []; + + function browserEventHandler(e) { + return actualBrowser.push(e); + } + on(browserEvents, "data", browserEventHandler); + on(windowEvents, "data", function handler(e) { + actualWindow.push(e); + // Delay close so that if "load" is also emitted on `browserEvents` + // `browserEventHandler` will be invoked. + if (e.type === "load") setTimeout(window.close); + if (e.type === "close") { + assert.deepEqual(actualBrowser, [], "browser events were not triggered"); + let [ open, ready, load, close ] = actualWindow; + + assert.equal(open.type, "open"); + assert.equal(open.target, window, "window is open"); + + + + assert.equal(ready.type, "DOMContentLoaded"); + assert.equal(ready.target, window, "window ready"); + + assert.equal(load.type, "load"); + assert.equal(load.target, window, "window load"); + + assert.equal(close.type, "close"); + assert.equal(close.target, window, "window load"); + + + // Note: If window is closed right after this GC won't have time + // to claim loader and there for this listener, there for it's safer + // to remove listener. + off(windowEvents, "data", handler); + off(browserEvents, "data", browserEventHandler); + loader.unload(); + done(); + } + }); + + // Open window and close it to trigger observers. + let window = open("data:text/html,not a browser"); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-buffer.js b/addon-sdk/source/test/test-buffer.js new file mode 100644 index 000000000..e55bf2b2f --- /dev/null +++ b/addon-sdk/source/test/test-buffer.js @@ -0,0 +1,563 @@ +/* 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/. */ + + +/* + * Many of these tests taken from Joyent's Node + * https://github.com/joyent/node/blob/master/test/simple/test-buffer.js + */ + +// 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. + +const { Buffer, TextEncoder, TextDecoder } = require('sdk/io/buffer'); +const { safeMerge } = require('sdk/util/object'); + +const ENCODINGS = ['utf-8']; + +exports.testBufferMain = function (assert) { + let b = Buffer('abcdef'); + + // try to create 0-length buffers + new Buffer(''); + new Buffer(0); + // test encodings supported by node; + // this is different than what node supports, details + // in buffer.js + ENCODINGS.forEach(enc => { + new Buffer('', enc); + assert.pass('Creating a buffer with ' + enc + ' does not throw'); + }); + + ENCODINGS.forEach(function(encoding) { + // Does not work with utf8 + if (encoding === 'utf-8') return; + var b = new Buffer(10); + b.write('あいうえお', encoding); + assert.equal(b.toString(encoding), 'あいうえお', + 'encode and decodes buffer with ' + encoding); + }); + + // invalid encoding for Buffer.toString + assert.throws(() => { + b.toString('invalid'); + }, RangeError, 'invalid encoding for Buffer.toString'); + + // try to toString() a 0-length slice of a buffer, both within and without the + // valid buffer range + assert.equal(new Buffer('abc').toString('utf8', 0, 0), '', + 'toString 0-length buffer, valid range'); + assert.equal(new Buffer('abc').toString('utf8', -100, -100), '', + 'toString 0-length buffer, invalid range'); + assert.equal(new Buffer('abc').toString('utf8', 100, 100), '', + 'toString 0-length buffer, invalid range'); + + // try toString() with a object as a encoding + assert.equal(new Buffer('abc').toString({toString: function() { + return 'utf8'; + }}), 'abc', 'toString with object as an encoding'); + + // test for buffer overrun + var buf = new Buffer([0, 0, 0, 0, 0]); // length: 5 + var sub = buf.slice(0, 4); // length: 4 + var written = sub.write('12345', 'utf8'); + assert.equal(written, 4, 'correct bytes written in slice'); + assert.equal(buf[4], 0, 'correct origin buffer value'); + + // Check for fractional length args, junk length args, etc. + // https://github.com/joyent/node/issues/1758 + Buffer(3.3).toString(); // throws bad argument error in commit 43cb4ec + assert.equal(Buffer(-1).length, 0); + assert.equal(Buffer(NaN).length, 0); + assert.equal(Buffer(3.3).length, 3); + assert.equal(Buffer({length: 3.3}).length, 3); + assert.equal(Buffer({length: 'BAM'}).length, 0); + + // Make sure that strings are not coerced to numbers. + assert.equal(Buffer('99').length, 2); + assert.equal(Buffer('13.37').length, 5); +}; + +exports.testIsEncoding = function (assert) { + ENCODINGS.map(encoding => { + assert.ok(Buffer.isEncoding(encoding), + 'Buffer.isEncoding ' + encoding + ' truthy'); + }); + ['not-encoding', undefined, null, 100, {}].map(encoding => { + assert.ok(!Buffer.isEncoding(encoding), + 'Buffer.isEncoding ' + encoding + ' falsy'); + }); +}; + +exports.testBufferCopy = function (assert) { + // counter to ensure unique value is always copied + var cntr = 0; + var b = Buffer(1024); // safe constructor + + assert.strictEqual(1024, b.length); + b[0] = -1; + assert.strictEqual(b[0], 255); + + var shimArray = []; + for (var i = 0; i < 1024; i++) { + b[i] = i % 256; + shimArray[i] = i % 256; + } + + compareBuffers(assert, b, shimArray, 'array notation'); + + var c = new Buffer(512); + assert.strictEqual(512, c.length); + // copy 512 bytes, from 0 to 512. + b.fill(++cntr); + c.fill(++cntr); + var copied = b.copy(c, 0, 0, 512); + assert.strictEqual(512, copied, + 'copied ' + copied + ' bytes from b into c'); + + compareBuffers(assert, b, c, 'copied to other buffer'); + + // copy c into b, without specifying sourceEnd + b.fill(++cntr); + c.fill(++cntr); + var copied = c.copy(b, 0, 0); + assert.strictEqual(c.length, copied, + 'copied ' + copied + ' bytes from c into b w/o sourceEnd'); + compareBuffers(assert, b, c, + 'copied to other buffer without specifying sourceEnd'); + + // copy c into b, without specifying sourceStart + b.fill(++cntr); + c.fill(++cntr); + var copied = c.copy(b, 0); + assert.strictEqual(c.length, copied, + 'copied ' + copied + ' bytes from c into b w/o sourceStart'); + compareBuffers(assert, b, c, + 'copied to other buffer without specifying sourceStart'); + + // copy longer buffer b to shorter c without targetStart + b.fill(++cntr); + c.fill(++cntr); + + var copied = b.copy(c); + assert.strictEqual(c.length, copied, + 'copied ' + copied + ' bytes from b into c w/o targetStart'); + compareBuffers(assert, b, c, + 'copy long buffer to shorter buffer without targetStart'); + + // copy starting near end of b to c + b.fill(++cntr); + c.fill(++cntr); + var copied = b.copy(c, 0, b.length - Math.floor(c.length / 2)); + assert.strictEqual(Math.floor(c.length / 2), copied, + 'copied ' + copied + ' bytes from end of b into beg. of c'); + + let successStatus = true; + for (var i = 0; i < Math.floor(c.length / 2); i++) { + if (b[b.length - Math.floor(c.length / 2) + i] !== c[i]) + successStatus = false; + } + + for (var i = Math.floor(c.length /2) + 1; i < c.length; i++) { + if (c[c.length-1] !== c[i]) + successStatus = false; + } + assert.ok(successStatus, + 'Copied bytes from end of large buffer into beginning of small buffer'); + + // try to copy 513 bytes, and check we don't overrun c + b.fill(++cntr); + c.fill(++cntr); + var copied = b.copy(c, 0, 0, 513); + assert.strictEqual(c.length, copied, + 'copied ' + copied + ' bytes from b trying to overrun c'); + compareBuffers(assert, b, c, + 'copying to buffer that would overflow'); + + // copy 768 bytes from b into b + b.fill(++cntr); + b.fill(++cntr, 256); + var copied = b.copy(b, 0, 256, 1024); + assert.strictEqual(768, copied, + 'copied ' + copied + ' bytes from b into b'); + + compareBuffers(assert, b, shimArray.map(()=>cntr), + 'copy partial buffer to itself'); + + // copy string longer than buffer length (failure will segfault) + var bb = new Buffer(10); + bb.fill('hello crazy world'); + + // copy throws at negative sourceStart + assert.throws(function() { + Buffer(5).copy(Buffer(5), 0, -1); + }, RangeError, 'buffer copy throws at negative sourceStart'); + + // check sourceEnd resets to targetEnd if former is greater than the latter + b.fill(++cntr); + c.fill(++cntr); + var copied = b.copy(c, 0, 0, 1025); + assert.strictEqual(copied, c.length, + 'copied ' + copied + ' bytes from b into c'); + compareBuffers(assert, b, c, 'copying should reset sourceEnd if targetEnd if sourceEnd > targetEnd'); + + // throw with negative sourceEnd + assert.throws(function() { + b.copy(c, 0, 0, -1); + }, RangeError, 'buffer copy throws at negative sourceEnd'); + + // when sourceStart is greater than sourceEnd, zero copied + assert.equal(b.copy(c, 0, 100, 10), 0); + + // when targetStart > targetLength, zero copied + assert.equal(b.copy(c, 512, 0, 10), 0); + + // try to copy 0 bytes worth of data into an empty buffer + b.copy(new Buffer(0), 0, 0, 0); + + // try to copy 0 bytes past the end of the target buffer + b.copy(new Buffer(0), 1, 1, 1); + b.copy(new Buffer(1), 1, 1, 1); + + // try to copy 0 bytes from past the end of the source buffer + b.copy(new Buffer(1), 0, 2048, 2048); +}; + +exports.testBufferWrite = function (assert) { + let b = Buffer(1024); + b.fill(0); + + // try to write a 0-length string beyond the end of b + assert.throws(function() { + b.write('', 2048); + }, RangeError, 'writing a 0-length string beyond buffer throws'); + // throw when writing to negative offset + assert.throws(function() { + b.write('a', -1); + }, RangeError, 'writing negative offset on buffer throws'); + + // throw when writing past bounds from the pool + assert.throws(function() { + b.write('a', 2048); + }, RangeError, 'writing past buffer bounds from pool throws'); + + // testing for smart defaults and ability to pass string values as offset + + // previous write API was the following: + // write(string, encoding, offset, length) + // this is planned on being removed in node v0.13, + // we will not support it + var writeTest = new Buffer('abcdes'); + writeTest.write('n', 'utf8'); +// writeTest.write('o', 'utf8', '1'); + writeTest.write('d', '2', 'utf8'); + writeTest.write('e', 3, 'utf8'); +// writeTest.write('j', 'utf8', 4); + assert.equal(writeTest.toString(), 'nbdees', + 'buffer write API alternative syntax works'); +}; + +exports.testBufferWriteEncoding = function (assert) { + + // Node #1210 Test UTF-8 string includes null character + var buf = new Buffer('\0'); + assert.equal(buf.length, 1); + buf = new Buffer('\0\0'); + assert.equal(buf.length, 2); + + buf = new Buffer(2); + var written = buf.write(''); // 0byte + assert.equal(written, 0); + written = buf.write('\0'); // 1byte (v8 adds null terminator) + assert.equal(written, 1); + written = buf.write('a\0'); // 1byte * 2 + assert.equal(written, 2); + // TODO, these tests write 0, possibly due to character encoding +/* + written = buf.write('あ'); // 3bytes + assert.equal(written, 0); + written = buf.write('\0あ'); // 1byte + 3bytes + assert.equal(written, 1); +*/ + written = buf.write('\0\0あ'); // 1byte * 2 + 3bytes + buf = new Buffer(10); + written = buf.write('あいう'); // 3bytes * 3 (v8 adds null terminator) + assert.equal(written, 9); + written = buf.write('あいう\0'); // 3bytes * 3 + 1byte + assert.equal(written, 10); +}; + +exports.testBufferWriteWithMaxLength = function (assert) { + // Node #243 Test write() with maxLength + var buf = new Buffer(4); + buf.fill(0xFF); + var written = buf.write('abcd', 1, 2, 'utf8'); + assert.equal(written, 2); + assert.equal(buf[0], 0xFF); + assert.equal(buf[1], 0x61); + assert.equal(buf[2], 0x62); + assert.equal(buf[3], 0xFF); + + buf.fill(0xFF); + written = buf.write('abcd', 1, 4); + assert.equal(written, 3); + assert.equal(buf[0], 0xFF); + assert.equal(buf[1], 0x61); + assert.equal(buf[2], 0x62); + assert.equal(buf[3], 0x63); + + buf.fill(0xFF); + // Ignore legacy API + /* + written = buf.write('abcd', 'utf8', 1, 2); // legacy style + console.log(buf); + assert.equal(written, 2); + assert.equal(buf[0], 0xFF); + assert.equal(buf[1], 0x61); + assert.equal(buf[2], 0x62); + assert.equal(buf[3], 0xFF); + */ +}; + +exports.testBufferSlice = function (assert) { + var asciiString = 'hello world'; + var offset = 100; + var b = Buffer(1024); + b.fill(0); + + for (var i = 0; i < asciiString.length; i++) { + b[i] = asciiString.charCodeAt(i); + } + var asciiSlice = b.toString('utf8', 0, asciiString.length); + assert.equal(asciiString, asciiSlice); + + var written = b.write(asciiString, offset, 'utf8'); + assert.equal(asciiString.length, written); + asciiSlice = b.toString('utf8', offset, offset + asciiString.length); + assert.equal(asciiString, asciiSlice); + + var sliceA = b.slice(offset, offset + asciiString.length); + var sliceB = b.slice(offset, offset + asciiString.length); + compareBuffers(assert, sliceA, sliceB, + 'slicing is idempotent'); + + let sliceTest = true; + for (var j = 0; j < 100; j++) { + var slice = b.slice(100, 150); + if (50 !== slice.length) + sliceTest = false; + for (var i = 0; i < 50; i++) { + if (b[100 + i] !== slice[i]) + sliceTest = false; + } + } + assert.ok(sliceTest, 'massive slice runs do not affect buffer'); + + // Single argument slice + let testBuf = new Buffer('abcde'); + assert.equal('bcde', testBuf.slice(1).toString(), 'single argument slice'); + + // slice(0,0).length === 0 + assert.equal(0, Buffer('hello').slice(0, 0).length, 'slice(0,0) === 0'); + + var buf = new Buffer('0123456789'); + assert.equal(buf.slice(-10, 10), '0123456789', 'buffer slice range correct'); + assert.equal(buf.slice(-20, 10), '0123456789', 'buffer slice range correct'); + assert.equal(buf.slice(-20, -10), '', 'buffer slice range correct'); + assert.equal(buf.slice(0, -1), '012345678', 'buffer slice range correct'); + assert.equal(buf.slice(2, -2), '234567', 'buffer slice range correct'); + assert.equal(buf.slice(0, 65536), '0123456789', 'buffer slice range correct'); + assert.equal(buf.slice(65536, 0), '', 'buffer slice range correct'); + + sliceTest = true; + for (var i = 0, s = buf.toString(); i < buf.length; ++i) { + if (buf.slice(-i) != s.slice(-i)) sliceTest = false; + if (buf.slice(0, -i) != s.slice(0, -i)) sliceTest = false; + } + assert.ok(sliceTest, 'buffer.slice should be consistent'); + + // Make sure modifying a sliced buffer, affects original and vice versa + b.fill(0); + let sliced = b.slice(0, 10); + let babyslice = sliced.slice(0, 5); + + for (let i = 0; i < sliced.length; i++) + sliced[i] = 'jetpack'.charAt(i); + + compareBuffers(assert, b, sliced, + 'modifying sliced buffer affects original'); + + compareBuffers(assert, b, babyslice, + 'modifying sliced buffer affects child-sliced buffer'); + + for (let i = 0; i < sliced.length; i++) + b[i] = 'odinmonkey'.charAt(i); + + compareBuffers(assert, b, sliced, + 'modifying original buffer affects sliced'); + + compareBuffers(assert, b, babyslice, + 'modifying original buffer affects grandchild sliced buffer'); +}; + +exports.testSlicingParents = function (assert) { + let root = Buffer(5); + let child = root.slice(0, 4); + let grandchild = child.slice(0, 3); + + assert.equal(root.parent, undefined, 'a new buffer should not have a parent'); + + // make sure a zero length slice doesn't set the .parent attribute + assert.equal(root.slice(0,0).parent, undefined, + '0-length slice should not have a parent'); + + assert.equal(child.parent, root, + 'a valid slice\'s parent should be the original buffer (child)'); + + assert.equal(grandchild.parent, root, + 'a valid slice\'s parent should be the original buffer (grandchild)'); +}; + +exports.testIsBuffer = function (assert) { + let buffer = new Buffer('content', 'utf8'); + assert.ok(Buffer.isBuffer(buffer), 'isBuffer truthy on buffers'); + assert.ok(!Buffer.isBuffer({}), 'isBuffer falsy on objects'); + assert.ok(!Buffer.isBuffer(new Uint8Array()), + 'isBuffer falsy on Uint8Array'); + assert.ok(Buffer.isBuffer(buffer.slice(0)), 'Buffer#slice should be a new buffer'); +}; + +exports.testBufferConcat = function (assert) { + let zero = []; + let one = [ new Buffer('asdf') ]; + let long = []; + for (let i = 0; i < 10; i++) long.push(new Buffer('asdf')); + + let flatZero = Buffer.concat(zero); + let flatOne = Buffer.concat(one); + let flatLong = Buffer.concat(long); + let flatLongLen = Buffer.concat(long, 40); + + assert.equal(flatZero.length, 0); + assert.equal(flatOne.toString(), 'asdf'); + assert.equal(flatOne, one[0]); + assert.equal(flatLong.toString(), (new Array(10+1).join('asdf'))); + assert.equal(flatLongLen.toString(), (new Array(10+1).join('asdf'))); +}; + +exports.testBufferByteLength = function (assert) { + let str = '\u00bd + \u00bc = \u00be'; + assert.equal(Buffer.byteLength(str), 12, + 'correct byteLength of string'); + + assert.equal(14, Buffer.byteLength('Il était tué')); + assert.equal(14, Buffer.byteLength('Il était tué', 'utf8')); + // We do not currently support these encodings + /* + ['ucs2', 'ucs-2', 'utf16le', 'utf-16le'].forEach(function(encoding) { + assert.equal(24, Buffer.byteLength('Il était tué', encoding)); + }); + assert.equal(12, Buffer.byteLength('Il était tué', 'ascii')); + assert.equal(12, Buffer.byteLength('Il était tué', 'binary')); + */ +}; + +exports.testTextEncoderDecoder = function (assert) { + assert.ok(TextEncoder, 'TextEncoder exists'); + assert.ok(TextDecoder, 'TextDecoder exists'); +}; + +exports.testOverflowedBuffers = function (assert) { + assert.throws(function() { + new Buffer(0xFFFFFFFF); + }, RangeError, 'correctly throws buffer overflow'); + + assert.throws(function() { + new Buffer(0xFFFFFFFFF); + }, RangeError, 'correctly throws buffer overflow'); + + assert.throws(function() { + var buf = new Buffer(8); + buf.readFloatLE(0xffffffff); + }, RangeError, 'correctly throws buffer overflow with readFloatLE'); + + assert.throws(function() { + var buf = new Buffer(8); + buf.writeFloatLE(0.0, 0xffffffff); + }, RangeError, 'correctly throws buffer overflow with writeFloatLE'); + + //ensure negative values can't get past offset + assert.throws(function() { + var buf = new Buffer(8); + buf.readFloatLE(-1); + }, RangeError, 'correctly throws with readFloatLE negative values'); + + assert.throws(function() { + var buf = new Buffer(8); + buf.writeFloatLE(0.0, -1); + }, RangeError, 'correctly throws with writeFloatLE with negative values'); + + assert.throws(function() { + var buf = new Buffer(8); + buf.readFloatLE(-1); + }, RangeError, 'correctly throws with readFloatLE with negative values'); +}; + +exports.testReadWriteDataTypeErrors = function (assert) { + var buf = new Buffer(0); + assert.throws(function() { buf.readUInt8(0); }, RangeError, + 'readUInt8(0) throws'); + assert.throws(function() { buf.readInt8(0); }, RangeError, + 'readInt8(0) throws'); + + [16, 32].forEach(function(bits) { + var buf = new Buffer(bits / 8 - 1); + assert.throws(function() { buf['readUInt' + bits + 'BE'](0); }, + RangeError, + 'readUInt' + bits + 'BE'); + + assert.throws(function() { buf['readUInt' + bits + 'LE'](0); }, + RangeError, + 'readUInt' + bits + 'LE'); + + assert.throws(function() { buf['readInt' + bits + 'BE'](0); }, + RangeError, + 'readInt' + bits + 'BE()'); + + assert.throws(function() { buf['readInt' + bits + 'LE'](0); }, + RangeError, + 'readInt' + bits + 'LE()'); + }); +}; + +safeMerge(exports, require('./buffers/test-write-types')); +safeMerge(exports, require('./buffers/test-read-types')); + +function compareBuffers (assert, buf1, buf2, message) { + let status = true; + for (let i = 0; i < Math.min(buf1.length, buf2.length); i++) { + if (buf1[i] !== buf2[i]) + status = false; + } + assert.ok(status, 'buffer successfully copied: ' + message); +} +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-byte-streams.js b/addon-sdk/source/test/test-byte-streams.js new file mode 100644 index 000000000..7d45130aa --- /dev/null +++ b/addon-sdk/source/test/test-byte-streams.js @@ -0,0 +1,169 @@ +/* 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 byteStreams = require("sdk/io/byte-streams"); +const file = require("sdk/io/file"); +const { pathFor } = require("sdk/system"); +const { Loader } = require("sdk/test/loader"); + +const STREAM_CLOSED_ERROR = new RegExp("The stream is closed and cannot be used."); + +// This should match the constant of the same name in byte-streams.js. +const BUFFER_BYTE_LEN = 0x8000; + +exports.testWriteRead = function (assert) { + let fname = dataFileFilename(); + + // Write a small string less than the stream's buffer size... + let str = "exports.testWriteRead data!"; + let stream = open(assert, fname, true); + assert.ok(!stream.closed, "stream.closed after open should be false"); + stream.write(str); + stream.close(); + assert.ok(stream.closed, "Stream should be closed after stream.close"); + assert.throws(() => stream.write("This shouldn't be written!"), + STREAM_CLOSED_ERROR, + "stream.write after close should raise error"); + + // ... and read it. + stream = open(assert, fname); + assert.equal(stream.read(), str, + "stream.read should return string written"); + assert.equal(stream.read(), "", + "stream.read at EOS should return empty string"); + stream.close(); + assert.ok(stream.closed, "Stream should be closed after stream.close"); + assert.throws(() => stream.read(), + STREAM_CLOSED_ERROR, + "stream.read after close should raise error"); + + file.remove(fname); +}; + +// Write a big string many times the size of the stream's buffer and read it. +exports.testWriteReadBig = function (assert) { + let str = ""; + let bufLen = BUFFER_BYTE_LEN; + let fileSize = bufLen * 10; + for (let i = 0; i < fileSize; i++) + str += i % 10; + let fname = dataFileFilename(); + let stream = open(assert, fname, true); + stream.write(str); + stream.close(); + stream = open(assert, fname); + assert.equal(stream.read(), str, + "stream.read should return string written"); + stream.close(); + file.remove(fname); +}; + +// The same, but write and read in chunks. +exports.testWriteReadChunks = function (assert) { + let str = ""; + let bufLen = BUFFER_BYTE_LEN; + let fileSize = bufLen * 10; + for (let i = 0; i < fileSize; i++) + str += i % 10; + let fname = dataFileFilename(); + let stream = open(assert, fname, true); + let i = 0; + while (i < str.length) { + // Use a chunk length that spans buffers. + let chunk = str.substr(i, bufLen + 1); + stream.write(chunk); + i += bufLen + 1; + } + stream.close(); + stream = open(assert, fname); + let readStr = ""; + bufLen = BUFFER_BYTE_LEN; + let readLen = bufLen + 1; + do { + var frag = stream.read(readLen); + readStr += frag; + } while (frag); + stream.close(); + assert.equal(readStr, str, + "stream.write and read in chunks should work as expected"); + file.remove(fname); +}; + +exports.testReadLengths = function (assert) { + let fname = dataFileFilename(); + let str = "exports.testReadLengths data!"; + let stream = open(assert, fname, true); + stream.write(str); + stream.close(); + + stream = open(assert, fname); + assert.equal(stream.read(str.length * 1000), str, + "stream.read with big byte length should return string " + + "written"); + stream.close(); + + stream = open(assert, fname); + assert.equal(stream.read(0), "", + "string.read with zero byte length should return empty " + + "string"); + stream.close(); + + stream = open(assert, fname); + assert.equal(stream.read(-1), "", + "string.read with negative byte length should return " + + "empty string"); + stream.close(); + + file.remove(fname); +}; + +exports.testTruncate = function (assert) { + let fname = dataFileFilename(); + let str = "exports.testReadLengths data!"; + let stream = open(assert, fname, true); + stream.write(str); + stream.close(); + + stream = open(assert, fname); + assert.equal(stream.read(), str, + "stream.read should return string written"); + stream.close(); + + stream = open(assert, fname, true); + stream.close(); + + stream = open(assert, fname); + assert.equal(stream.read(), "", + "stream.read after truncate should be empty"); + stream.close(); + + file.remove(fname); +}; + +exports.testUnload = function (assert) { + let loader = Loader(module); + let file = loader.require("sdk/io/file"); + + let filename = dataFileFilename("temp-b"); + let stream = file.open(filename, "wb"); + + loader.unload(); + assert.ok(stream.closed, "Stream should be closed after module unload"); +}; + +// Returns the name of a file that should be used to test writing and reading. +function dataFileFilename() { + return file.join(pathFor("ProfD"), "test-byte-streams-data"); +} + +// Opens and returns the given file and ensures it's of the correct class. +function open(assert, filename, forWriting) { + let stream = file.open(filename, forWriting ? "wb" : "b"); + let klass = forWriting ? "ByteWriter" : "ByteReader"; + assert.ok(stream instanceof byteStreams[klass], + "Opened stream should be a " + klass); + return stream; +} + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-child_process.js b/addon-sdk/source/test/test-child_process.js new file mode 100644 index 000000000..4cfd9ec49 --- /dev/null +++ b/addon-sdk/source/test/test-child_process.js @@ -0,0 +1,545 @@ +/* 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 { spawn, exec, execFile, fork } = require('sdk/system/child_process'); +const { env, platform, pathFor } = require('sdk/system'); +const { isNumber } = require('sdk/lang/type'); +const { after } = require('sdk/test/utils'); +const { emit } = require('sdk/event/core'); +const PROFILE_DIR= pathFor('ProfD'); +const isWindows = platform.toLowerCase().indexOf('win') === 0; +const { getScript, cleanUp } = require('./fixtures/child-process-scripts'); + +// We use direct paths to these utilities as we currently cannot +// call non-absolute paths to utilities in subprocess.jsm +const CAT_PATH = isWindows ? 'C:\\Windows\\System32\\more.com' : '/bin/cat'; + +exports.testExecCallbackSuccess = function (assert, done) { + exec(isWindows ? 'DIR /A-D' : 'ls -al', { + cwd: PROFILE_DIR + }, function (err, stdout, stderr) { + assert.ok(!err, 'no errors found'); + assert.equal(stderr, '', 'stderr is empty'); + assert.ok(/extensions\.ini/.test(stdout), 'stdout output of `ls -al` finds files'); + + if (isWindows) { + // `DIR /A-D` does not display directories on WIN + assert.ok(!//.test(stdout), + 'passing arguments in `exec` works'); + } + else { + // `ls -al` should list all the priviledge information on Unix + assert.ok(/d(r[-|w][-|x]){3}/.test(stdout), + 'passing arguments in `exec` works'); + } + done(); + }); +}; + +exports.testExecCallbackError = function (assert, done) { + exec('not-real-command', { cwd: PROFILE_DIR }, function (err, stdout, stderr) { + assert.ok(/not-real-command/.test(err.toString()), + 'error contains error message'); + assert.ok(err.lineNumber >= 0, 'error contains lineNumber'); + assert.ok(/resource:\/\//.test(err.fileName), 'error contains fileName'); + assert.ok(err.code && isNumber(err.code), 'non-zero error code property on error'); + assert.equal(err.signal, null, + 'null signal property when not manually terminated'); + assert.equal(stdout, '', 'stdout is empty'); + assert.ok(/not-real-command/.test(stderr), 'stderr contains error message'); + done(); + }); +}; + +exports.testExecOptionsEnvironment = function (assert, done) { + getScript('check-env').then(envScript => { + exec(envScript, { + cwd: PROFILE_DIR, + env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' } + }, function (err, stdout, stderr) { + assert.equal(stderr, '', 'stderr is empty'); + assert.ok(!err, 'received `cwd` option'); + assert.ok(/my-value-test/.test(stdout), + 'receives environment option'); + done(); + }); + }).then(null, assert.fail); +}; + +exports.testExecOptionsTimeout = function (assert, done) { + let count = 0; + getScript('wait').then(script => { + let child = exec(script, { timeout: 100 }, (err, stdout, stderr) => { + assert.equal(err.killed, true, 'error has `killed` property as true'); + assert.equal(err.code, null, 'error has `code` as null'); + assert.equal(err.signal, 'SIGTERM', + 'error has `signal` as SIGTERM by default'); + assert.equal(stdout, '', 'stdout is empty'); + assert.equal(stderr, '', 'stderr is empty'); + if (++count === 3) complete(); + }); + + function exitHandler (code, signal) { + assert.equal(code, null, 'error has `code` as null'); + assert.equal(signal, 'SIGTERM', + 'error has `signal` as SIGTERM by default'); + if (++count === 3) complete(); + } + + function closeHandler (code, signal) { + assert.equal(code, null, 'error has `code` as null'); + assert.equal(signal, 'SIGTERM', + 'error has `signal` as SIGTERM by default'); + if (++count === 3) complete(); + } + + child.on('exit', exitHandler); + child.on('close', closeHandler); + + function complete () { + child.off('exit', exitHandler); + child.off('close', closeHandler); + done(); + } + }).then(null, assert.fail); +}; + +exports.testExecFileCallbackSuccess = function (assert, done) { + getScript('args').then(script => { + execFile(script, ['--myargs', '-j', '-s'], { cwd: PROFILE_DIR }, function (err, stdout, stderr) { + assert.ok(!err, 'no errors found'); + assert.equal(stderr, '', 'stderr is empty'); + // Trim output since different systems have different new line output + assert.equal(stdout.trim(), '--myargs -j -s'.trim(), 'passes in correct arguments'); + done(); + }); + }).then(null, assert.fail); +}; + +exports.testExecFileCallbackError = function (assert, done) { + execFile('not-real-command', { cwd: PROFILE_DIR }, function (err, stdout, stderr) { + assert.ok(/Executable not found/.test(err.message), + `error '${err.message}' contains error message`); + assert.ok(err.lineNumber >= 0, 'error contains lineNumber'); + assert.ok(/resource:\/\//.test(err.fileName), 'error contains fileName'); + assert.equal(stdout, '', 'stdout is empty'); + assert.equal(stderr, '', 'stdout is empty'); + done(); + }); +}; + +exports.testExecFileOptionsEnvironment = function (assert, done) { + getScript('check-env').then(script => { + execFile(script, { + cwd: PROFILE_DIR, + env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' } + }, function (err, stdout, stderr) { + assert.equal(stderr, '', 'stderr is empty'); + assert.ok(!err, 'received `cwd` option'); + assert.ok(/my-value-test/.test(stdout), + 'receives environment option'); + done(); + }); + }).then(null, assert.fail); +}; + +exports.testExecFileOptionsTimeout = function (assert, done) { + let count = 0; + getScript('wait').then(script => { + let child = execFile(script, { timeout: 100 }, (err, stdout, stderr) => { + assert.equal(err.killed, true, 'error has `killed` property as true'); + assert.equal(err.code, null, 'error has `code` as null'); + assert.equal(err.signal, 'SIGTERM', + 'error has `signal` as SIGTERM by default'); + assert.equal(stdout, '', 'stdout is empty'); + assert.equal(stderr, '', 'stderr is empty'); + if (++count === 3) complete(); + }); + + function exitHandler (code, signal) { + assert.equal(code, null, 'error has `code` as null'); + assert.equal(signal, 'SIGTERM', + 'error has `signal` as SIGTERM by default'); + if (++count === 3) complete(); + } + + function closeHandler (code, signal) { + assert.equal(code, null, 'error has `code` as null'); + assert.equal(signal, 'SIGTERM', + 'error has `signal` as SIGTERM by default'); + if (++count === 3) complete(); + } + + child.on('exit', exitHandler); + child.on('close', closeHandler); + + function complete () { + child.off('exit', exitHandler); + child.off('close', closeHandler); + done(); + } + }).then(null, assert.fail); +}; + +/** + * Not necessary to test for both `exec` and `execFile`, but + * it is necessary to test both when the buffer is larger + * and smaller than buffer size used by the subprocess library (1024) + */ +exports.testExecFileOptionsMaxBufferLargeStdOut = function (assert, done) { + let count = 0; + let stdoutChild; + + // Creates a buffer of 2000 to stdout, greater than 1024 + getScript('large-out').then(script => { + stdoutChild = execFile(script, ['10000'], { maxBuffer: 50 }, (err, stdout, stderr) => { + assert.ok(/stdout maxBuffer exceeded/.test(err.toString()), + 'error contains stdout maxBuffer exceeded message'); + assert.ok(stdout.length >= 50, 'stdout has full buffer'); + assert.equal(stderr, '', 'stderr is empty'); + if (++count === 3) complete(); + }); + stdoutChild.on('exit', exitHandler); + stdoutChild.on('close', closeHandler); + }).then(null, assert.fail); + + function exitHandler (code, signal) { + assert.equal(code, null, 'Exit code is null in exit handler'); + assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler'); + if (++count === 3) complete(); + } + + function closeHandler (code, signal) { + assert.equal(code, null, 'Exit code is null in close handler'); + assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler'); + if (++count === 3) complete(); + } + + function complete () { + stdoutChild.off('exit', exitHandler); + stdoutChild.off('close', closeHandler); + done(); + } +}; + +exports.testExecFileOptionsMaxBufferLargeStdOErr = function (assert, done) { + let count = 0; + let stderrChild; + // Creates a buffer of 2000 to stderr, greater than 1024 + getScript('large-err').then(script => { + stderrChild = execFile(script, ['10000'], { maxBuffer: 50 }, (err, stdout, stderr) => { + assert.ok(/stderr maxBuffer exceeded/.test(err.toString()), + 'error contains stderr maxBuffer exceeded message'); + assert.ok(stderr.length >= 50, 'stderr has full buffer'); + assert.equal(stdout, '', 'stdout is empty'); + if (++count === 3) complete(); + }); + stderrChild.on('exit', exitHandler); + stderrChild.on('close', closeHandler); + }).then(null, assert.fail); + + function exitHandler (code, signal) { + assert.equal(code, null, 'Exit code is null in exit handler'); + assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler'); + if (++count === 3) complete(); + } + + function closeHandler (code, signal) { + assert.equal(code, null, 'Exit code is null in close handler'); + assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler'); + if (++count === 3) complete(); + } + + function complete () { + stderrChild.off('exit', exitHandler); + stderrChild.off('close', closeHandler); + done(); + } +}; + +/** + * When total buffer is < process buffer (1024), the process will exit + * and not get a chance to be killed for violating the maxBuffer, + * although the error will still be sent through (node behaviour) + */ +exports.testExecFileOptionsMaxBufferSmallStdOut = function (assert, done) { + let count = 0; + let stdoutChild; + + // Creates a buffer of 60 to stdout, less than 1024 + getScript('large-out').then(script => { + stdoutChild = execFile(script, ['60'], { maxBuffer: 50 }, (err, stdout, stderr) => { + assert.ok(/stdout maxBuffer exceeded/.test(err.toString()), + 'error contains stdout maxBuffer exceeded message'); + assert.ok(stdout.length >= 50, 'stdout has full buffer'); + assert.equal(stderr, '', 'stderr is empty'); + if (++count === 3) complete(); + }); + stdoutChild.on('exit', exitHandler); + stdoutChild.on('close', closeHandler); + }).then(null, assert.fail); + + function exitHandler (code, signal) { + // Sometimes the buffer limit is hit before the process closes successfully + // on both OSX/Windows + if (code === null) { + assert.equal(code, null, 'Exit code is null in exit handler'); + assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler'); + } + else { + assert.equal(code, 0, 'Exit code is 0 in exit handler'); + assert.equal(signal, null, 'Signal is null in exit handler'); + } + if (++count === 3) complete(); + } + + function closeHandler (code, signal) { + // Sometimes the buffer limit is hit before the process closes successfully + // on both OSX/Windows + if (code === null) { + assert.equal(code, null, 'Exit code is null in close handler'); + assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler'); + } + else { + assert.equal(code, 0, 'Exit code is 0 in close handler'); + assert.equal(signal, null, 'Signal is null in close handler'); + } + if (++count === 3) complete(); + } + + function complete () { + stdoutChild.off('exit', exitHandler); + stdoutChild.off('close', closeHandler); + done(); + } +}; + +exports.testExecFileOptionsMaxBufferSmallStdErr = function (assert, done) { + let count = 0; + let stderrChild; + // Creates a buffer of 60 to stderr, less than 1024 + getScript('large-err').then(script => { + stderrChild = execFile(script, ['60'], { maxBuffer: 50 }, (err, stdout, stderr) => { + assert.ok(/stderr maxBuffer exceeded/.test(err.toString()), + 'error contains stderr maxBuffer exceeded message'); + assert.ok(stderr.length >= 50, 'stderr has full buffer'); + assert.equal(stdout, '', 'stdout is empty'); + if (++count === 3) complete(); + }); + stderrChild.on('exit', exitHandler); + stderrChild.on('close', closeHandler); + }).then(null, assert.fail); + + function exitHandler (code, signal) { + // Sometimes the buffer limit is hit before the process closes successfully + // on both OSX/Windows + if (code === null) { + assert.equal(code, null, 'Exit code is null in exit handler'); + assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in exit handler'); + } + else { + assert.equal(code, 0, 'Exit code is 0 in exit handler'); + assert.equal(signal, null, 'Signal is null in exit handler'); + } + if (++count === 3) complete(); + } + + function closeHandler (code, signal) { + // Sometimes the buffer limit is hit before the process closes successfully + // on both OSX/Windows + if (code === null) { + assert.equal(code, null, 'Exit code is null in close handler'); + assert.equal(signal, 'SIGTERM', 'Signal is SIGTERM in close handler'); + } + else { + assert.equal(code, 0, 'Exit code is 0 in close handler'); + assert.equal(signal, null, 'Signal is null in close handler'); + } + if (++count === 3) complete(); + } + + function complete () { + stderrChild.off('exit', exitHandler); + stderrChild.off('close', closeHandler); + done(); + } +}; + +exports.testChildExecFileKillSignal = function (assert, done) { + getScript('wait').then(script => { + execFile(script, { + killSignal: 'beepbeep', + timeout: 10 + }, function (err, stdout, stderr) { + assert.equal(err.signal, 'beepbeep', 'correctly used custom killSignal'); + done(); + }); + }).then(null, assert.fail); +}; + +exports.testChildProperties = function (assert, done) { + getScript('check-env').then(script => { + let child = spawn(script, { + env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' } + }); + + if (isWindows) + assert.ok(true, 'Windows environment does not have `pid`'); + else + assert.ok(child.pid > 0, 'Child has a pid'); + }).then(done, assert.fail); +}; + +exports.testChildStdinStreamLarge = function (assert, done) { + let REPEAT = 2000; + let allData = ''; + // Use direct paths to more/cat, as we do not currently support calling non-files + // from subprocess.jsm + let child = spawn(CAT_PATH); + + child.stdout.on('data', onData); + child.on('close', onClose); + + for (let i = 0; i < REPEAT; i++) + emit(child.stdin, 'data', '12345\n'); + + emit(child.stdin, 'end'); + + function onData (data) { + allData += data; + } + + function onClose (code, signal) { + child.stdout.off('data', onData); + child.off('close', onClose); + assert.equal(code, 0, 'exited succesfully'); + assert.equal(signal, null, 'no kill signal given'); + assert.equal(allData.replace(/\W/g, '').length, '12345'.length * REPEAT, + 'all data processed from stdin'); + done(); + } +}; + +exports.testChildStdinStreamSmall = function (assert, done) { + let allData = ''; + let child = spawn(CAT_PATH); + child.stdout.on('data', onData); + child.on('close', onClose); + + emit(child.stdin, 'data', '12345'); + emit(child.stdin, 'end'); + + function onData (data) { + allData += data; + } + + function onClose (code, signal) { + child.stdout.off('data', onData); + child.off('close', onClose); + assert.equal(code, 0, 'exited succesfully'); + assert.equal(signal, null, 'no kill signal given'); + assert.equal(allData.trim(), '12345', 'all data processed from stdin'); + done(); + } +}; +/* + * This tests failures when an error is thrown attempting to + * spawn the process, like an invalid command + */ +exports.testChildEventsSpawningError= function (assert, done) { + let handlersCalled = 0; + let child = execFile('i-do-not-exist', (err, stdout, stderr) => { + assert.ok(err, 'error was passed into callback'); + assert.equal(stdout, '', 'stdout is empty') + assert.equal(stderr, '', 'stderr is empty'); + if (++handlersCalled === 3) complete(); + }); + + child.on('error', handleError); + child.on('exit', handleExit); + child.on('close', handleClose); + + function handleError (e) { + assert.ok(e, 'error passed into error handler'); + if (++handlersCalled === 3) complete(); + } + + function handleClose (code, signal) { + assert.equal(code, -1, + 'process was never spawned, therefore exit code is -1'); + assert.equal(signal, null, 'signal should be null'); + if (++handlersCalled === 3) complete(); + } + + function handleExit (code, signal) { + assert.fail('Close event should not be called on init failure'); + } + + function complete () { + child.off('error', handleError); + child.off('exit', handleExit); + child.off('close', handleClose); + done(); + } +}; + +exports.testSpawnOptions = function (assert, done) { + let count = 0; + let envStdout = ''; + let cwdStdout = ''; + let checkEnv, checkPwd, envChild, cwdChild; + getScript('check-env').then(script => { + checkEnv = script; + return getScript('check-pwd'); + }).then(script => { + checkPwd = script; + + envChild = spawn(checkEnv, { + env: { CHILD_PROCESS_ENV_TEST: 'my-value-test' } + }); + cwdChild = spawn(checkPwd, { cwd: PROFILE_DIR }); + + // Do these need to be unbound? + envChild.stdout.on('data', data => envStdout += data); + cwdChild.stdout.on('data', data => cwdStdout += data); + + envChild.on('close', envClose); + cwdChild.on('close', cwdClose); + }).then(null, assert.fail); + + function envClose () { + assert.equal(envStdout.trim(), 'my-value-test', 'spawn correctly passed in ENV'); + if (++count === 2) complete(); + } + + function cwdClose () { + // Check for PROFILE_DIR in the output because + // some systems resolve symbolic links, and on OSX + // /var -> /private/var + let isCorrectPath = ~cwdStdout.trim().indexOf(PROFILE_DIR); + assert.ok(isCorrectPath, 'spawn correctly passed in cwd'); + if (++count === 2) complete(); + } + + function complete () { + envChild.off('close', envClose); + cwdChild.off('close', cwdClose); + done(); + } +}; + +exports.testFork = function (assert) { + assert.throws(function () { + fork(); + }, /not currently supported/, 'fork() correctly throws an unsupported error'); +}; + +after(exports, cleanUp); + +require("sdk/test").run(exports); + +// Test disabled because of bug 979675 +module.exports = {}; diff --git a/addon-sdk/source/test/test-chrome.js b/addon-sdk/source/test/test-chrome.js new file mode 100644 index 000000000..3d1f29dd0 --- /dev/null +++ b/addon-sdk/source/test/test-chrome.js @@ -0,0 +1,84 @@ +/* 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 chrome = require('chrome'); + +const FIXTURES_URL = module.uri.substr(0, module.uri.lastIndexOf('/') + 1) + + 'fixtures/chrome-worker/' + +exports['test addEventListener'] = function(assert, done) { + let uri = FIXTURES_URL + 'addEventListener.js'; + + let worker = new chrome.ChromeWorker(uri); + worker.addEventListener('message', function(event) { + assert.equal(event.data, 'Hello', 'message received'); + worker.terminate(); + done(); + }); +}; + +exports['test onmessage'] = function(assert, done) { + let uri = FIXTURES_URL + 'onmessage.js'; + + let worker = new chrome.ChromeWorker(uri); + worker.onmessage = function(event) { + assert.equal(event.data, 'ok', 'message received'); + worker.terminate(); + done(); + }; + worker.postMessage('ok'); +}; + +exports['test setTimeout'] = function(assert, done) { + let uri = FIXTURES_URL + 'setTimeout.js'; + + let worker = new chrome.ChromeWorker(uri); + worker.onmessage = function(event) { + assert.equal(event.data, 'ok', 'setTimeout fired'); + worker.terminate(); + done(); + }; +}; + +exports['test jsctypes'] = function(assert, done) { + let uri = FIXTURES_URL + 'jsctypes.js'; + + let worker = new chrome.ChromeWorker(uri); + worker.onmessage = function(event) { + assert.equal(event.data, 'function', 'ctypes.open is a function'); + worker.terminate(); + done(); + }; +}; + +exports['test XMLHttpRequest'] = function(assert, done) { + let uri = FIXTURES_URL + 'xhr.js'; + + let worker = new chrome.ChromeWorker(uri); + worker.onmessage = function(event) { + assert.equal(event.data, 'ok', 'XMLHttpRequest works'); + worker.terminate(); + done(); + }; +}; + +exports['test onerror'] = function(assert, done) { + let uri = FIXTURES_URL + 'onerror.js'; + + let worker = new chrome.ChromeWorker(uri); + worker.onerror = function(event) { + assert.equal(event.filename, uri, 'event reports the correct uri'); + assert.equal(event.lineno, 6, 'event reports the correct line number'); + assert.equal(event.target, worker, 'event reports the correct worker'); + assert.ok(event.message.match(/ok/), + 'event contains the exception message'); + // Call preventDefault in order to avoid being displayed in JS console. + event.preventDefault(); + worker.terminate(); + done(); + }; +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-clipboard.js b/addon-sdk/source/test/test-clipboard.js new file mode 100644 index 000000000..f7ffd05be --- /dev/null +++ b/addon-sdk/source/test/test-clipboard.js @@ -0,0 +1,170 @@ +/* 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"; + +require("sdk/clipboard"); + +const { Cc, Ci } = require("chrome"); + +const imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools); +const io = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); +const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].getService(Ci.nsIAppShellService); + +const XHTML_NS = "http://www.w3.org/1999/xhtml"; +const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" + + "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" + + "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" + + "bWRR9AAAAABJRU5ErkJggg%3D%3D"; + +const { base64jpeg } = require("./fixtures"); + +const { platform } = require("sdk/system"); +// For Windows, Mac and Linux, platform returns the following: winnt, darwin and linux. +var isWindows = platform.toLowerCase().indexOf("win") == 0; + +// Test the typical use case, setting & getting with no flavors specified +exports["test With No Flavor"] = function(assert) { + var contents = "hello there"; + var flavor = "text"; + var fullFlavor = "text/unicode"; + var clip = require("sdk/clipboard"); + + // Confirm we set the clipboard + assert.ok(clip.set(contents)); + + // Confirm flavor is set + assert.equal(clip.currentFlavors[0], flavor); + + // Confirm we set the clipboard + assert.equal(clip.get(), contents); + + // Confirm we can get the clipboard using the flavor + assert.equal(clip.get(flavor), contents); + + // Confirm we can still get the clipboard using the full flavor + assert.equal(clip.get(fullFlavor), contents); +}; + +// Test the slightly less common case where we specify the flavor +exports["test With Flavor"] = function(assert) { + var contents = "hello there"; + var contentsText = "hello there"; + + // On windows, HTML clipboard includes extra data. + // The values are from widget/windows/nsDataObj.cpp. + var contentsWindowsHtml = "\n" + + contents + + "\n\n"; + + var flavor = "html"; + var fullFlavor = "text/html"; + var unicodeFlavor = "text"; + var unicodeFullFlavor = "text/unicode"; + var clip = require("sdk/clipboard"); + + assert.ok(clip.set(contents, flavor)); + + assert.equal(clip.currentFlavors[0], unicodeFlavor); + assert.equal(clip.currentFlavors[1], flavor); + assert.equal(clip.get(), contentsText); + assert.equal(clip.get(flavor), isWindows ? contentsWindowsHtml : contents); + assert.equal(clip.get(fullFlavor), isWindows ? contentsWindowsHtml : contents); + assert.equal(clip.get(unicodeFlavor), contentsText); + assert.equal(clip.get(unicodeFullFlavor), contentsText); +}; + +// Test that the typical case still works when we specify the flavor to set +exports["test With Redundant Flavor"] = function(assert) { + var contents = "hello there"; + var flavor = "text"; + var fullFlavor = "text/unicode"; + var clip = require("sdk/clipboard"); + + assert.ok(clip.set(contents, flavor)); + assert.equal(clip.currentFlavors[0], flavor); + assert.equal(clip.get(), contents); + assert.equal(clip.get(flavor), contents); + assert.equal(clip.get(fullFlavor), contents); +}; + +exports["test Not In Flavor"] = function(assert) { + var contents = "hello there"; + var flavor = "html"; + var clip = require("sdk/clipboard"); + + assert.ok(clip.set(contents)); + // If there's nothing on the clipboard with this flavor, should return null + assert.equal(clip.get(flavor), null); +}; + +exports["test Set Image"] = function(assert) { + var clip = require("sdk/clipboard"); + var flavor = "image"; + var fullFlavor = "image/png"; + + assert.ok(clip.set(base64png, flavor), "clipboard set"); + assert.equal(clip.currentFlavors[0], flavor, "flavor is set"); +}; + +exports["test Get Image"] = function* (assert) { + var clip = require("sdk/clipboard"); + + clip.set(base64png, "image"); + + var contents = clip.get(); + const hiddenWindow = appShellService.hiddenDOMWindow; + const Image = hiddenWindow.Image; + const canvas = hiddenWindow.document.createElementNS(XHTML_NS, "canvas"); + let context = canvas.getContext("2d"); + + const imageURLToPixels = (imageURL) => { + return new Promise((resolve) => { + let img = new Image(); + + img.onload = function() { + context.drawImage(this, 0, 0); + + let pixels = Array.join(context.getImageData(0, 0, 32, 32).data); + resolve(pixels); + }; + + img.src = imageURL; + }); + }; + + let [base64pngPixels, clipboardPixels] = yield Promise.all([ + imageURLToPixels(base64png), imageURLToPixels(contents), + ]); + + assert.ok(base64pngPixels === clipboardPixels, + "Image gets from clipboard equals to image sets to the clipboard"); +}; + +exports["test Set Image Type Not Supported"] = function(assert) { + var clip = require("sdk/clipboard"); + var flavor = "image"; + + assert.throws(function () { + clip.set(base64jpeg, flavor); + }, "Invalid flavor for image/jpeg"); + +}; + +// Notice that `imageTools.decodeImageData`, used by `clipboard.set` method for +// images, write directly to the javascript console the error in case the image +// is corrupt, even if the error is catched. +// +// See: http://mxr.mozilla.org/mozilla-central/source/image/src/Decoder.cpp#136 +exports["test Set Image Type Wrong Data"] = function(assert) { + var clip = require("sdk/clipboard"); + var flavor = "image"; + + var wrongPNG = "data:image/png" + base64jpeg.substr(15); + + assert.throws(function () { + clip.set(wrongPNG, flavor); + }, "Unable to decode data given in a valid image."); +}; + +require("sdk/test").run(exports) diff --git a/addon-sdk/source/test/test-collection.js b/addon-sdk/source/test/test-collection.js new file mode 100644 index 000000000..d723c14ce --- /dev/null +++ b/addon-sdk/source/test/test-collection.js @@ -0,0 +1,128 @@ +/* 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 collection = require("sdk/util/collection"); + +exports.testAddRemove = function (assert) { + let coll = new collection.Collection(); + compare(assert, coll, []); + addRemove(assert, coll, [], false); +}; + +exports.testAddRemoveBackingArray = function (assert) { + let items = ["foo"]; + let coll = new collection.Collection(items); + compare(assert, coll, items); + addRemove(assert, coll, items, true); + + items = ["foo", "bar"]; + coll = new collection.Collection(items); + compare(assert, coll, items); + addRemove(assert, coll, items, true); +}; + +exports.testProperty = function (assert) { + let obj = makeObjWithCollProp(); + compare(assert, obj.coll, []); + addRemove(assert, obj.coll, [], false); + + // Test single-value set. + let items = ["foo"]; + obj.coll = items[0]; + compare(assert, obj.coll, items); + addRemove(assert, obj.coll, items, false); + + // Test array set. + items = ["foo", "bar"]; + obj.coll = items; + compare(assert, obj.coll, items); + addRemove(assert, obj.coll, items, false); +}; + +exports.testPropertyBackingArray = function (assert) { + let items = ["foo"]; + let obj = makeObjWithCollProp(items); + compare(assert, obj.coll, items); + addRemove(assert, obj.coll, items, true); + + items = ["foo", "bar"]; + obj = makeObjWithCollProp(items); + compare(assert, obj.coll, items); + addRemove(assert, obj.coll, items, true); +}; + +// Adds some values to coll and then removes them. initialItems is an array +// containing coll's initial items. isBacking is true if initialItems is coll's +// backing array; the point is that updates to coll should affect initialItems +// if that's the case. +function addRemove(assert, coll, initialItems, isBacking) { + let items = isBacking ? initialItems : initialItems.slice(0); + let numInitialItems = items.length; + + // Test add(val). + let numInsertions = 5; + for (let i = 0; i < numInsertions; i++) { + compare(assert, coll, items); + coll.add(i); + if (!isBacking) + items.push(i); + } + compare(assert, coll, items); + + // Add the items we just added to make sure duplicates aren't added. + for (let i = 0; i < numInsertions; i++) + coll.add(i); + compare(assert, coll, items); + + // Test remove(val). Do a kind of shuffled remove. Remove item 1, then + // item 0, 3, 2, 5, 4, ... + for (let i = 0; i < numInsertions; i++) { + let val = i % 2 ? i - 1 : + i === numInsertions - 1 ? i : i + 1; + coll.remove(val); + if (!isBacking) + items.splice(items.indexOf(val), 1); + compare(assert, coll, items); + } + assert.equal(coll.length, numInitialItems, + "All inserted items should be removed"); + + // Remove the items we just removed. coll should be unchanged. + for (let i = 0; i < numInsertions; i++) + coll.remove(i); + compare(assert, coll, items); + + // Test add and remove([val1, val2]). + let newItems = [0, 1]; + coll.add(newItems); + compare(assert, coll, isBacking ? items : items.concat(newItems)); + coll.remove(newItems); + compare(assert, coll, items); + assert.equal(coll.length, numInitialItems, + "All inserted items should be removed"); +} + +// Asserts that the items in coll are the items of array. +function compare(assert, coll, array) { + assert.equal(coll.length, array.length, + "Collection length should be correct"); + let numItems = 0; + for (let item in coll) { + assert.equal(item, array[numItems], "Items should be equal"); + numItems++; + } + assert.equal(numItems, array.length, + "Number of items in iteration should be correct"); +} + +// Returns a new object with a collection property named "coll". backingArray, +// if defined, will create the collection with that backing array. +function makeObjWithCollProp(backingArray) { + let obj = {}; + collection.addCollectionProperty(obj, "coll", backingArray); + return obj; +} + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-commonjs-test-adapter.js b/addon-sdk/source/test/test-commonjs-test-adapter.js new file mode 100644 index 000000000..936bea918 --- /dev/null +++ b/addon-sdk/source/test/test-commonjs-test-adapter.js @@ -0,0 +1,11 @@ +/* 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 custom `Assert`'s"] = require("./commonjs-test-adapter/asserts"); + +// Disabling this check since it is not yet supported by jetpack. +// if (module == require.main) + require("test").run(exports); diff --git a/addon-sdk/source/test/test-content-events.js b/addon-sdk/source/test/test-content-events.js new file mode 100644 index 000000000..059c356c4 --- /dev/null +++ b/addon-sdk/source/test/test-content-events.js @@ -0,0 +1,92 @@ +/* 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("sdk/test/loader"); +const { getMostRecentBrowserWindow, getInnerId } = require("sdk/window/utils"); +const { openTab, closeTab, getBrowserForTab } = require("sdk/tabs/utils"); +const { defer } = require("sdk/core/promise"); +const { curry, identity, partial } = require("sdk/lang/functional"); + +const { nuke } = require("sdk/loader/sandbox"); + +const { open: openWindow, close: closeWindow } = require('sdk/window/helpers'); + +const openBrowserWindow = partial(openWindow, null, {features: {toolbar: true}}); + +var when = curry(function(options, tab) { + let type = options.type || options; + let capture = options.capture || false; + let target = getBrowserForTab(tab); + let { promise, resolve } = defer(); + + target.addEventListener(type, function handler(event) { + if (!event.target.defaultView.frameElement) { + target.removeEventListener(type, handler, capture); + resolve(tab); + } + }, capture); + + return promise; +}); + +var use = use = value => () => value; + + +var open = curry((url, window) => openTab(window, url)); +var close = function(tab) { + let promise = when("pagehide", tab); + closeTab(tab); + return promise; +} + +exports["test dead object errors"] = function(assert, done) { + let system = require("sdk/system/events"); + let loader = Loader(module); + let { events } = loader.require("sdk/content/events"); + + // The dead object error is properly reported on console but + // doesn't raise any test's exception + function onMessage({ subject }) { + let message = subject.wrappedJSObject; + let { level } = message; + let text = String(message.arguments[0]); + + if (level === "error" && text.includes("can't access dead object")) + fail(text); + } + + let cleanup = () => system.off("console-api-log-event", onMessage); + let fail = (reason) => { + cleanup(); + assert.fail(reason); + } + + loader.unload(); + + nuke(loader.sharedGlobalSandbox); + + system.on("console-api-log-event", onMessage, true); + + openBrowserWindow(). + then(closeWindow). + then(() => assert.pass("checking dead object errors")). + then(cleanup). + then(done, fail); +}; + +// ignore *-document-global-created events that are not very consistent. +// only allow data uris that we create to ignore unwanted events, e.g., +// about:blank, http:// requests from Fennec's `about:`home page that displays +// add-ons a user could install, local `searchplugins`, other chrome uris +// Calls callback if passes filter +function eventFilter (type, target, callback) { + if (target.URL.startsWith("data:text/html,") && + type !== "chrome-document-global-created" && + type !== "content-document-global-created") + + callback(); +} +require("test").run(exports); diff --git a/addon-sdk/source/test/test-content-script.js b/addon-sdk/source/test/test-content-script.js new file mode 100644 index 000000000..d3c7e1bbc --- /dev/null +++ b/addon-sdk/source/test/test-content-script.js @@ -0,0 +1,845 @@ +/* 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 */ + +var 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 +var html = ' +

+ +

+ + A targetted link. + +

+ +

+ +

+ +

+ + A link with no ID and an anchor, used by PredicateContext tests. + +

+ +

+ + + +

+

+ + + + + A complex nested structure. + + + + +

+ +

+ +

+ +

+ +

+ +

+ +

+ +

+

This content is editable.

+

+ + diff --git a/addon-sdk/source/test/test-context-menu.js b/addon-sdk/source/test/test-context-menu.js new file mode 100644 index 000000000..f1a955545 --- /dev/null +++ b/addon-sdk/source/test/test-context-menu.js @@ -0,0 +1,3763 @@ +/* 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'; + +require("sdk/context-menu"); + +const { defer } = require("sdk/core/promise"); +const { isTravisCI } = require("sdk/test/utils"); +const packaging = require('@loader/options'); + +// These should match the same constants in the module. +const OVERFLOW_THRESH_DEFAULT = 10; +const OVERFLOW_THRESH_PREF = + "extensions.addon-sdk.context-menu.overflowThreshold"; + +const TEST_DOC_URL = module.uri.replace(/\.js$/, ".html"); +const data = require("./fixtures"); + +const { TestHelper } = require("./context-menu/test-helper.js") + +// Tests that when present the separator is placed before the separator from +// the old context-menu module +exports.testSeparatorPosition = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + // Create the old separator + let oldSeparator = test.contextMenuPopup.ownerDocument.createElement("menuseparator"); + oldSeparator.id = "jetpack-context-menu-separator"; + test.contextMenuPopup.appendChild(oldSeparator); + + // Create an item. + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + assert.equal(test.contextMenuSeparator.nextSibling.nextSibling, oldSeparator, + "New separator should appear before the old one"); + test.contextMenuPopup.removeChild(oldSeparator); + test.done(); + }); +}; + +// Destroying items that were previously created should cause them to be absent +// from the menu. +exports.testConstructDestroy = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + // Create an item. + let item = new loader.cm.Item({ label: "item" }); + assert.equal(item.parentMenu, loader.cm.contentContextMenu, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + + // It should be present when the menu is shown. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Destroy the item. Multiple destroys should be harmless. + item.destroy(); + item.destroy(); + test.showMenu(null, function (popup) { + + // It should be removed from the menu. + test.checkMenu([item], [], [item]); + test.done(); + }); + }); +}; + + +// Destroying an item twice should not cause an error. +exports.testDestroyTwice = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + item.destroy(); + item.destroy(); + + test.pass("Destroying an item twice should not cause an error."); + test.done(); +}; + + +// CSS selector contexts should cause their items to be present in the menu +// when the menu is invoked on nodes that match the selectors. +exports.testSelectorContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("img") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// CSS selector contexts should cause their items to be present in the menu +// when the menu is invoked on nodes that have ancestors that match the +// selectors. +exports.testSelectorAncestorContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("a[href]") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#span-link", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// CSS selector contexts should cause their items to be absent from the menu +// when the menu is not invoked on nodes that match or have ancestors that +// match the selectors. +exports.testSelectorContextNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item", + context: loader.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); +}; + + +// Page contexts should cause their items to be present in the menu when the +// menu is not invoked on an active element. +exports.testPageContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 0" + }), + new loader.cm.Item({ + label: "item 1", + context: undefined + }), + new loader.cm.Item({ + label: "item 2", + context: loader.cm.PageContext() + }), + new loader.cm.Item({ + label: "item 3", + context: [loader.cm.PageContext()] + }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Page contexts should cause their items to be absent from the menu when the +// menu is invoked on an active element. +exports.testPageContextNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 0" + }), + new loader.cm.Item({ + label: "item 1", + context: undefined + }), + new loader.cm.Item({ + label: "item 2", + context: loader.cm.PageContext() + }), + new loader.cm.Item({ + label: "item 3", + context: [loader.cm.PageContext()] + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + + +// Selection contexts should cause items to appear when a selection exists. +exports.testSelectionContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + window.getSelection().selectAllChildren(doc.body); + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Selection contexts should cause items to appear when a selection exists in +// a text field. +exports.testSelectionContextMatchInTextField = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + test.selectRange("#textfield", 0, null); + test.showMenu("#textfield", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Selection contexts should not cause items to appear when a selection does +// not exist in a text field. +exports.testSelectionContextNoMatchInTextField = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + test.selectRange("#textfield", 0, 0); + test.showMenu("#textfield", function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); +}; + + +// Selection contexts should not cause items to appear when a selection does +// not exist. +exports.testSelectionContextNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); +}; + + +// Selection contexts should cause items to appear when a selection exists even +// for newly opened pages +exports.testSelectionContextInNewTab = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + let link = doc.getElementById("targetlink"); + link.click(); + + let tablistener = event => { + this.tabBrowser.tabContainer.removeEventListener("TabOpen", tablistener, false); + let tab = event.target; + let browser = tab.linkedBrowser; + this.loadFrameScript(browser); + this.delayedEventListener(browser, "load", () => { + let window = browser.contentWindow; + let doc = browser.contentDocument; + window.getSelection().selectAllChildren(doc.body); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + + test.tabBrowser.removeTab(test.tabBrowser.selectedTab); + test.tabBrowser.selectedTab = test.tab; + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); + }, true); + }; + this.tabBrowser.tabContainer.addEventListener("TabOpen", tablistener, false); + }); +}; + + +// Selection contexts should work when right clicking a form button +exports.testSelectionContextButtonMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + window.getSelection().selectAllChildren(doc.body); + test.showMenu("#button", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +//Selection contexts should work when right clicking a form button +exports.testSelectionContextButtonNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "item", + context: loader.cm.SelectionContext() + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#button", function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); +}; + + +// URL contexts should cause items to appear on pages that match. +exports.testURLContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + loader.cm.Item({ + label: "item 0", + context: loader.cm.URLContext(TEST_DOC_URL) + }), + loader.cm.Item({ + label: "item 1", + context: loader.cm.URLContext([TEST_DOC_URL, "*.bogus.com"]) + }), + loader.cm.Item({ + label: "item 2", + context: loader.cm.URLContext([new RegExp(".*\\.html")]) + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// URL contexts should not cause items to appear on pages that do not match. +exports.testURLContextNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + loader.cm.Item({ + label: "item 0", + context: loader.cm.URLContext("*.bogus.com") + }), + loader.cm.Item({ + label: "item 1", + context: loader.cm.URLContext(["*.bogus.com", "*.gnarly.com"]) + }), + loader.cm.Item({ + label: "item 2", + context: loader.cm.URLContext([new RegExp(".*\\.js")]) + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + + +// Loading a new page in the same tab should correctly start a new worker for +// any content scripts +exports.testPageReload = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ + label: "Item", + contentScript: "var doc = document; self.on('context', node => doc.body.getAttribute('showItem') == 'true');" + }); + + test.withTestDoc(function (window, doc) { + // Set a flag on the document that the item uses + doc.body.setAttribute("showItem", "true"); + + test.showMenu(null, function (popup) { + // With the attribute true the item should be visible in the menu + test.checkMenu([item], [], []); + test.hideMenu(function() { + let browser = this.tabBrowser.getBrowserForTab(this.tab) + test.delayedEventListener(browser, "load", function() { + test.delayedEventListener(browser, "load", function() { + window = browser.contentWindow; + doc = window.document; + + // Set a flag on the document that the item uses + doc.body.setAttribute("showItem", "false"); + + test.showMenu(null, function (popup) { + // In the new document with the attribute false the item should be + // hidden, but if the contentScript hasn't been reloaded it will + // still see the old value + test.checkMenu([item], [item], []); + + test.done(); + }); + }, true); + browser.loadURI(TEST_DOC_URL, null, null); + }, true); + // Required to make sure we load a new page in history rather than + // just reloading the current page which would unload it + browser.loadURI("about:blank", null, null); + }); + }); + }); +}; + +// Closing a page after it's been used with a worker should cause the worker +// to be destroyed +/*exports.testWorkerDestroy = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let loadExpected = false; + + let item = loader.cm.Item({ + label: "item", + contentScript: 'self.postMessage("loaded"); self.on("detach", function () { console.log("saw detach"); self.postMessage("detach") });', + onMessage: function (msg) { + switch (msg) { + case "loaded": + assert.ok(loadExpected, "Should have seen the load event at the right time"); + loadExpected = false; + break; + case "detach": + test.done(); + break; + } + } + }); + + test.withTestDoc(function (window, doc) { + loadExpected = true; + test.showMenu(null, function (popup) { + assert.ok(!loadExpected, "Should have seen a message"); + + test.checkMenu([item], [], []); + + test.closeTab(); + }); + }); +};*/ + + +// Content contexts that return true should cause their items to be present +// in the menu. +exports.testContentContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => true);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// Content contexts that return false should cause their items to be absent +// from the menu. +exports.testContentContextNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => false);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); +}; + + +// Content contexts that return undefined should cause their items to be absent +// from the menu. +exports.testContentContextUndefined = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function () {});' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); +}; + + +// Content contexts that return an empty string should cause their items to be +// absent from the menu and shouldn't wipe the label +exports.testContentContextEmptyString = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => "");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [item], []); + assert.equal(item.label, "item", "Label should still be correct"); + test.done(); + }); +}; + + +// If any content contexts returns true then their items should be present in +// the menu. +exports.testMultipleContentContextMatch1 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => true); ' + + 'self.on("context", () => false);', + onMessage: function() { + test.fail("Should not have called the second context listener"); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// If any content contexts returns true then their items should be present in +// the menu. +exports.testMultipleContentContextMatch2 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => false); ' + + 'self.on("context", () => true);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// If any content contexts returns a string then their items should be present +// in the menu. +exports.testMultipleContentContextString1 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => "new label"); ' + + 'self.on("context", () => false);' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + assert.equal(item.label, "new label", "Label should have changed"); + test.done(); + }); +}; + + +// If any content contexts returns a string then their items should be present +// in the menu. +exports.testMultipleContentContextString2 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => false); ' + + 'self.on("context", () => "new label");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + assert.equal(item.label, "new label", "Label should have changed"); + test.done(); + }); +}; + + +// If many content contexts returns a string then the first should take effect +exports.testMultipleContentContextString3 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", () => "new label 1"); ' + + 'self.on("context", () => "new label 2");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + assert.equal(item.label, "new label 1", "Label should have changed"); + test.done(); + }); +}; + + +// Content contexts that return true should cause their items to be present +// in the menu when context clicking an active element. +exports.testContentContextMatchActiveElement = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 1", + contentScript: 'self.on("context", () => true);' + }), + new loader.cm.Item({ + label: "item 2", + context: undefined, + contentScript: 'self.on("context", () => true);' + }), + // These items will always be hidden by the declarative usage of PageContext + new loader.cm.Item({ + label: "item 3", + context: loader.cm.PageContext(), + contentScript: 'self.on("context", () => true);' + }), + new loader.cm.Item({ + label: "item 4", + context: [loader.cm.PageContext()], + contentScript: 'self.on("context", () => true);' + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, [items[2], items[3]], []); + test.done(); + }); + }); +}; + + +// Content contexts that return false should cause their items to be absent +// from the menu when context clicking an active element. +exports.testContentContextNoMatchActiveElement = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 1", + contentScript: 'self.on("context", () => false);' + }), + new loader.cm.Item({ + label: "item 2", + context: undefined, + contentScript: 'self.on("context", () => false);' + }), + // These items will always be hidden by the declarative usage of PageContext + new loader.cm.Item({ + label: "item 3", + context: loader.cm.PageContext(), + contentScript: 'self.on("context", () => false);' + }), + new loader.cm.Item({ + label: "item 4", + context: [loader.cm.PageContext()], + contentScript: 'self.on("context", () => false);' + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + + +// Content contexts that return undefined should cause their items to be absent +// from the menu when context clicking an active element. +exports.testContentContextNoMatchActiveElement = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ + label: "item 1", + contentScript: 'self.on("context", () => {});' + }), + new loader.cm.Item({ + label: "item 2", + context: undefined, + contentScript: 'self.on("context", () => {});' + }), + // These items will always be hidden by the declarative usage of PageContext + new loader.cm.Item({ + label: "item 3", + context: loader.cm.PageContext(), + contentScript: 'self.on("context", () => {});' + }), + new loader.cm.Item({ + label: "item 4", + context: [loader.cm.PageContext()], + contentScript: 'self.on("context", () => {});' + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + + +// Content contexts that return a string should cause their items to be present +// in the menu and the items' labels to be updated. +exports.testContentContextMatchString = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "first label", + contentScript: 'self.on("context", () => "second label");' + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + assert.equal(item.label, "second label", + "item's label should be updated"); + test.done(); + }); +}; + + +// Ensure that contentScriptFile is working correctly +exports.testContentScriptFile = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let { defer, all } = require("sdk/core/promise"); + let itemScript = [defer(), defer()]; + let menuShown = defer(); + let menuPromises = itemScript.concat(menuShown).map(({promise}) => promise); + + // Reject remote files + assert.throws(function() { + new loader.cm.Item({ + label: "item", + contentScriptFile: "http://mozilla.com/context-menu.js" + }); + }, + /The `contentScriptFile` option must be a local URL or an array of URLs/, + "Item throws when contentScriptFile is a remote URL"); + + // But accept files from data folder + let item = new loader.cm.Item({ + label: "item", + contentScriptFile: data.url("test-contentScriptFile.js"), + onMessage: (message) => { + assert.equal(message, "msg from contentScriptFile", + "contentScriptFile loaded with absolute url"); + itemScript[0].resolve(); + } + }); + + let item2 = new loader.cm.Item({ + label: "item2", + contentScriptFile: "./test-contentScriptFile.js", + onMessage: (message) => { + assert.equal(message, "msg from contentScriptFile", + "contentScriptFile loaded with relative url"); + itemScript[1].resolve(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item, item2], [], []); + menuShown.resolve(); + }); + + all(menuPromises).then(() => test.done()); +}; + + +// The args passed to context listeners should be correct. +exports.testContentContextArgs = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let callbacks = 0; + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'self.on("context", function (node) {' + + ' self.postMessage(node.tagName);' + + ' return false;' + + '});', + onMessage: function (tagName) { + assert.equal(tagName, "HTML", "node should be an HTML element"); + if (++callbacks == 2) test.done(); + } + }); + + test.showMenu(null, function () { + if (++callbacks == 2) test.done(); + }); +}; + +// Multiple contexts imply intersection, not union. +exports.testMultipleContexts = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + context: [loader.cm.SelectorContext("a[href]"), loader.cm.PageContext()], + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#span-link", function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); +}; + +// Once a context is removed, it should no longer cause its item to appear. +exports.testRemoveContext = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let ctxt = loader.cm.SelectorContext("img"); + let item = new loader.cm.Item({ + label: "item", + context: ctxt + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + + // The item should be present at first. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Remove the img context and check again. + item.context.remove(ctxt); + test.showMenu("#image", function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); + }); +}; + +// Once a context is removed, it should no longer cause its item to appear. +exports.testSetContextRemove = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let ctxt = loader.cm.SelectorContext("img"); + let item = new loader.cm.Item({ + label: "item", + context: ctxt + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + + // The item should be present at first. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Remove the img context and check again. + item.context = []; + test.showMenu("#image", function (popup) { + test.checkMenu([item], [item], []); + test.done(); + }); + }); + }); +}; + +// Once a context is added, it should affect whether the item appears. +exports.testAddContext = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let ctxt = loader.cm.SelectorContext("img"); + let item = new loader.cm.Item({ + label: "item" + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + + // The item should not be present at first. + test.checkMenu([item], [item], []); + popup.hidePopup(); + + // Add the img context and check again. + item.context.add(ctxt); + test.showMenu("#image", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); + }); +}; + +// Once a context is added, it should affect whether the item appears. +exports.testSetContextAdd = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let ctxt = loader.cm.SelectorContext("img"); + let item = new loader.cm.Item({ + label: "item" + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + + // The item should not be present at first. + test.checkMenu([item], [item], []); + popup.hidePopup(); + + // Add the img context and check again. + item.context = [ctxt]; + test.showMenu("#image", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); + }); +}; + +// Lots of items should overflow into the overflow submenu. +exports.testOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT + 1; i++) { + let item = new loader.cm.Item({ label: "item " + i }); + items.push(item); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Module unload should cause all items to be removed. +exports.testUnload = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + + // The menu should contain the item. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Unload the module. + loader.unload(); + test.showMenu(null, function (popup) { + + // The item should be removed from the menu. + test.checkMenu([item], [], [item]); + test.done(); + }); + }); +}; + + +// Using multiple module instances to add items without causing overflow should +// work OK. Assumes OVERFLOW_THRESH_DEFAULT >= 2. +exports.testMultipleModulesAdd = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + // Use each module to add an item, then unload each module in turn. + let item0 = new loader0.cm.Item({ label: "item 0" }); + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain both items. + test.checkMenu([item0, item1], [], []); + popup.hidePopup(); + + // Unload the first module. + loader0.unload(); + test.showMenu(null, function (popup) { + + // The first item should be removed from the menu. + test.checkMenu([item0, item1], [], [item0]); + popup.hidePopup(); + + // Unload the second module. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([item0, item1], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to add items causing overflow should work OK. +exports.testMultipleModulesAddOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + // Use module 0 to add OVERFLOW_THRESH_DEFAULT items. + let items0 = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i++) { + let item = new loader0.cm.Item({ label: "item 0 " + i }); + items0.push(item); + } + + // Use module 1 to add one item. + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let allItems = items0.concat(item1); + + test.showMenu(null, function (popup) { + + // The menu should contain all items in overflow. + test.checkMenu(allItems, [], []); + popup.hidePopup(); + + // Unload the first module. + loader0.unload(); + test.showMenu(null, function (popup) { + + // The first items should be removed from the menu, which should not + // overflow. + test.checkMenu(allItems, [], items0); + popup.hidePopup(); + + // Unload the second module. + loader1.unload(); + test.showMenu(null, function (popup) { + + // All items should be removed from the menu. + test.checkMenu(allItems, [], allItems); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader0 create item -> loader1 create item -> loader0.unload -> +// loader1.unload +exports.testMultipleModulesDiffContexts1 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item0, item1], [item0], []); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // item0 should be removed from the menu. + test.checkMenu([item0, item1], [], [item0]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([item0, item1], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader1 create item -> loader0 create item -> loader0.unload -> +// loader1.unload +exports.testMultipleModulesDiffContexts2 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item0, item1], [item0], []); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // item0 should be removed from the menu. + test.checkMenu([item0, item1], [], [item0]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([item0, item1], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader0 create item -> loader1 create item -> loader1.unload -> +// loader0.unload +exports.testMultipleModulesDiffContexts3 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item0, item1], [item0], []); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // item1 should be removed from the menu. + test.checkMenu([item0, item1], [item0], [item1]); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([item0, item1], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Using multiple module instances to modify the menu without causing overflow +// should work OK. This test creates two loaders and: +// loader1 create item -> loader0 create item -> loader1.unload -> +// loader0.unload +exports.testMultipleModulesDiffContexts4 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item1 = new loader1.cm.Item({ label: "item 1" }); + + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("img") + }); + + test.showMenu(null, function (popup) { + + // The menu should contain item1. + test.checkMenu([item0, item1], [item0], []); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // item1 should be removed from the menu. + test.checkMenu([item0, item1], [item0], [item1]); + popup.hidePopup(); + + // Unload module 0. + loader0.unload(); + test.showMenu(null, function (popup) { + + // Both items should be removed from the menu. + test.checkMenu([item0, item1], [], [item0, item1]); + test.done(); + }); + }); + }); +}; + + +// Test interactions between a loaded module, unloading another module, and the +// menu separator and overflow submenu. +exports.testMultipleModulesAddRemove = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item = new loader0.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + + // The menu should contain the item. + test.checkMenu([item], [], []); + popup.hidePopup(); + + // Remove the item. + item.destroy(); + test.showMenu(null, function (popup) { + + // The item should be removed from the menu. + test.checkMenu([item], [], [item]); + popup.hidePopup(); + + // Unload module 1. + loader1.unload(); + test.showMenu(null, function (popup) { + + // There shouldn't be any errors involving the menu separator or + // overflow submenu. + test.checkMenu([item], [], [item]); + test.done(); + }); + }); + }); +}; + + +// Checks that the order of menu items is correct when adding/removing across +// multiple modules. All items from a single module should remain in a group +exports.testMultipleModulesOrder = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + // Use each module to add an item, then unload each module in turn. + let item0 = new loader0.cm.Item({ label: "item 0" }); + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain both items. + test.checkMenu([item0, item1], [], []); + popup.hidePopup(); + + let item2 = new loader0.cm.Item({ label: "item 2" }); + + test.showMenu(null, function (popup) { + + // The new item should be grouped with the same items from loader0. + test.checkMenu([item0, item2, item1], [], []); + popup.hidePopup(); + + let item3 = new loader1.cm.Item({ label: "item 3" }); + + test.showMenu(null, function (popup) { + + // Same again + test.checkMenu([item0, item2, item1, item3], [], []); + test.done(); + }); + }); + }); +}; + + +// Checks that the order of menu items is correct when adding/removing across +// multiple modules when overflowing. All items from a single module should +// remain in a group +exports.testMultipleModulesOrderOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + // Use each module to add an item, then unload each module in turn. + let item0 = new loader0.cm.Item({ label: "item 0" }); + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + + // The menu should contain both items. + test.checkMenu([item0, item1], [], []); + popup.hidePopup(); + + let item2 = new loader0.cm.Item({ label: "item 2" }); + + test.showMenu(null, function (popup) { + + // The new item should be grouped with the same items from loader0. + test.checkMenu([item0, item2, item1], [], []); + popup.hidePopup(); + + let item3 = new loader1.cm.Item({ label: "item 3" }); + + test.showMenu(null, function (popup) { + + // Same again + test.checkMenu([item0, item2, item1, item3], [], []); + test.done(); + }); + }); + }); +}; + + +// Checks that if a module's items are all hidden then the overflow menu doesn't +// get hidden +exports.testMultipleModulesOverflowHidden = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + // Use each module to add an item, then unload each module in turn. + let item0 = new loader0.cm.Item({ label: "item 0" }); + let item1 = new loader1.cm.Item({ + label: "item 1", + context: loader1.cm.SelectorContext("a") + }); + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu([item0, item1], [item1], []); + test.done(); + }); +}; + + +// Checks that if a module's items are all hidden then the overflow menu doesn't +// get hidden (reverse order to above) +exports.testMultipleModulesOverflowHidden2 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + // Use each module to add an item, then unload each module in turn. + let item0 = new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("a") + }); + let item1 = new loader1.cm.Item({ label: "item 1" }); + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu([item0, item1], [item0], []); + test.done(); + }); +}; + + +// Checks that we don't overflow if there are more items than the overflow +// threshold but not all of them are visible +exports.testOverflowIgnoresHidden = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let prefs = loader.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 2); + + let allItems = [ + new loader.cm.Item({ + label: "item 0" + }), + new loader.cm.Item({ + label: "item 1" + }), + new loader.cm.Item({ + label: "item 2", + context: loader.cm.SelectorContext("a") + }) + ]; + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu(allItems, [allItems[2]], []); + test.done(); + }); +}; + + +// Checks that we don't overflow if there are more items than the overflow +// threshold but not all of them are visible +exports.testOverflowIgnoresHiddenMultipleModules1 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 2); + + let allItems = [ + new loader0.cm.Item({ + label: "item 0" + }), + new loader0.cm.Item({ + label: "item 1" + }), + new loader1.cm.Item({ + label: "item 2", + context: loader1.cm.SelectorContext("a") + }), + new loader1.cm.Item({ + label: "item 3", + context: loader1.cm.SelectorContext("a") + }) + ]; + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu(allItems, [allItems[2], allItems[3]], []); + test.done(); + }); +}; + + +// Checks that we don't overflow if there are more items than the overflow +// threshold but not all of them are visible +exports.testOverflowIgnoresHiddenMultipleModules2 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 2); + + let allItems = [ + new loader0.cm.Item({ + label: "item 0" + }), + new loader0.cm.Item({ + label: "item 1", + context: loader0.cm.SelectorContext("a") + }), + new loader1.cm.Item({ + label: "item 2" + }), + new loader1.cm.Item({ + label: "item 3", + context: loader1.cm.SelectorContext("a") + }) + ]; + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu(allItems, [allItems[1], allItems[3]], []); + test.done(); + }); +}; + + +// Checks that we don't overflow if there are more items than the overflow +// threshold but not all of them are visible +exports.testOverflowIgnoresHiddenMultipleModules3 = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let prefs = loader0.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 2); + + let allItems = [ + new loader0.cm.Item({ + label: "item 0", + context: loader0.cm.SelectorContext("a") + }), + new loader0.cm.Item({ + label: "item 1", + context: loader0.cm.SelectorContext("a") + }), + new loader1.cm.Item({ + label: "item 2" + }), + new loader1.cm.Item({ + label: "item 3" + }) + ]; + + test.showMenu(null, function (popup) { + // One should be hidden + test.checkMenu(allItems, [allItems[0], allItems[1]], []); + test.done(); + }); +}; + + +// Tests that we transition between overflowing to non-overflowing to no items +// and back again +exports.testOverflowTransition = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let prefs = loader.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 2); + + let pItems = [ + new loader.cm.Item({ + label: "item 0", + context: loader.cm.SelectorContext("p") + }), + new loader.cm.Item({ + label: "item 1", + context: loader.cm.SelectorContext("p") + }) + ]; + + let aItems = [ + new loader.cm.Item({ + label: "item 2", + context: loader.cm.SelectorContext("a") + }), + new loader.cm.Item({ + label: "item 3", + context: loader.cm.SelectorContext("a") + }) + ]; + + let allItems = pItems.concat(aItems); + + test.withTestDoc(function (window, doc) { + test.showMenu("#link", function (popup) { + // The menu should contain all items and will overflow + test.checkMenu(allItems, [], []); + popup.hidePopup(); + + test.showMenu("#text", function (popup) { + // Only contains hald the items and will not overflow + test.checkMenu(allItems, aItems, []); + popup.hidePopup(); + + test.showMenu(null, function (popup) { + // None of the items will be visible + test.checkMenu(allItems, allItems, []); + popup.hidePopup(); + + test.showMenu("#text", function (popup) { + // Only contains hald the items and will not overflow + test.checkMenu(allItems, aItems, []); + popup.hidePopup(); + + test.showMenu("#link", function (popup) { + // The menu should contain all items and will overflow + test.checkMenu(allItems, [], []); + popup.hidePopup(); + + test.showMenu(null, function (popup) { + // None of the items will be visible + test.checkMenu(allItems, allItems, []); + popup.hidePopup(); + + test.showMenu("#link", function (popup) { + // The menu should contain all items and will overflow + test.checkMenu(allItems, [], []); + test.done(); + }); + }); + }); + }); + }); + }); + }); + }); +}; + + +// An item's command listener should work. +exports.testItemCommand = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item data", + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + assert.equal(this, item, "`this` inside onMessage should be item"); + assert.equal(data.tagName, "HTML", "node should be an HTML element"); + assert.equal(data.data, item.data, "data should be item data"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + let elt = test.getItemElt(popup, item); + + // create a command event + let evt = elt.ownerDocument.createEvent('Event'); + evt.initEvent('command', true, true); + elt.dispatchEvent(evt); + }); +}; + + +// A menu's click listener should work and receive bubbling 'command' events from +// sub-items appropriately. This also tests menus and ensures that when a CSS +// selector context matches the clicked node's ancestor, the matching ancestor +// is passed to listeners as the clicked node. +exports.testMenuCommand = function (assert, done) { + // Create a top-level menu, submenu, and item, like this: + // topMenu -> submenu -> item + // Click the item and make sure the click bubbles. + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "submenu item", + data: "submenu item data", + context: loader.cm.SelectorContext("a"), + }); + + let submenu = new loader.cm.Menu({ + label: "submenu", + context: loader.cm.SelectorContext("a"), + items: [item] + }); + + let topMenu = new loader.cm.Menu({ + label: "top menu", + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + assert.equal(this, topMenu, "`this` inside top menu should be menu"); + assert.equal(data.tagName, "A", "Clicked node should be anchor"); + assert.equal(data.data, item.data, + "Clicked item data should be correct"); + test.done(); + }, + items: [submenu], + context: loader.cm.SelectorContext("a") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#span-link", function (popup) { + test.checkMenu([topMenu], [], []); + let topMenuElt = test.getItemElt(popup, topMenu); + let topMenuPopup = topMenuElt.firstChild; + let submenuElt = test.getItemElt(topMenuPopup, submenu); + let submenuPopup = submenuElt.firstChild; + let itemElt = test.getItemElt(submenuPopup, item); + + // create a command event + let evt = itemElt.ownerDocument.createEvent('Event'); + evt.initEvent('command', true, true); + itemElt.dispatchEvent(evt); + }); + }); +}; + + +// Click listeners should work when multiple modules are loaded. +exports.testItemCommandMultipleModules = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = loader0.cm.Item({ + label: "loader 0 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.fail("loader 0 item should not emit click event"); + } + }); + let item1 = loader1.cm.Item({ + label: "loader 1 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.pass("loader 1 item clicked as expected"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item0, item1], [], []); + let item1Elt = test.getItemElt(popup, item1); + + // create a command event + let evt = item1Elt.ownerDocument.createEvent('Event'); + evt.initEvent('command', true, true); + item1Elt.dispatchEvent(evt); + }); +}; + + + + +// An item's click listener should work. +exports.testItemClick = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + data: "item data", + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + assert.equal(this, item, "`this` inside onMessage should be item"); + assert.equal(data.tagName, "HTML", "node should be an HTML element"); + assert.equal(data.data, item.data, "data should be item data"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + let elt = test.getItemElt(popup, item); + elt.click(); + }); +}; + + +// A menu's click listener should work and receive bubbling clicks from +// sub-items appropriately. This also tests menus and ensures that when a CSS +// selector context matches the clicked node's ancestor, the matching ancestor +// is passed to listeners as the clicked node. +exports.testMenuClick = function (assert, done) { + // Create a top-level menu, submenu, and item, like this: + // topMenu -> submenu -> item + // Click the item and make sure the click bubbles. + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "submenu item", + data: "submenu item data", + context: loader.cm.SelectorContext("a"), + }); + + let submenu = new loader.cm.Menu({ + label: "submenu", + context: loader.cm.SelectorContext("a"), + items: [item] + }); + + let topMenu = new loader.cm.Menu({ + label: "top menu", + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function (data) { + assert.equal(this, topMenu, "`this` inside top menu should be menu"); + assert.equal(data.tagName, "A", "Clicked node should be anchor"); + assert.equal(data.data, item.data, + "Clicked item data should be correct"); + test.done(); + }, + items: [submenu], + context: loader.cm.SelectorContext("a") + }); + + test.withTestDoc(function (window, doc) { + test.showMenu("#span-link", function (popup) { + test.checkMenu([topMenu], [], []); + let topMenuElt = test.getItemElt(popup, topMenu); + let topMenuPopup = topMenuElt.firstChild; + let submenuElt = test.getItemElt(topMenuPopup, submenu); + let submenuPopup = submenuElt.firstChild; + let itemElt = test.getItemElt(submenuPopup, item); + itemElt.click(); + }); + }); +}; + +// Click listeners should work when multiple modules are loaded. +exports.testItemClickMultipleModules = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = loader0.cm.Item({ + label: "loader 0 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.fail("loader 0 item should not emit click event"); + } + }); + let item1 = loader1.cm.Item({ + label: "loader 1 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.pass("loader 1 item clicked as expected"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item0, item1], [], []); + let item1Elt = test.getItemElt(popup, item1); + item1Elt.click(); + }); +}; + + +// Adding a separator to a submenu should work OK. +exports.testSeparator = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = new loader.cm.Menu({ + label: "submenu", + items: [new loader.cm.Separator()] + }); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// The parentMenu option should work +exports.testParentMenu = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = new loader.cm.Menu({ + label: "submenu", + items: [loader.cm.Item({ label: "item 1" })], + parentMenu: loader.cm.contentContextMenu + }); + + let item = loader.cm.Item({ + label: "item 2", + parentMenu: menu, + }); + + assert.equal(menu.items[1], item, "Item should be in the sub menu"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Existing context menu modifications should apply to new windows. +exports.testNewWindow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.withNewWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// When a new window is opened, items added by an unloaded module should not +// be present in the menu. +exports.testNewWindowMultipleModules = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + loader.unload(); + test.withNewWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([item], [], [item]); + test.done(); + }); + }); + }); +}; + + +// Existing context menu modifications should not apply to new private windows. +exports.testNewPrivateWindow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + + test.withNewPrivateWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([], [], []); + test.done(); + }); + }); + }); +}; + + +// Existing context menu modifications should apply to new private windows when +// private browsing support is enabled. +exports.testNewPrivateEnabledWindow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newPrivateLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + + test.withNewPrivateWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); + }); +}; + + +// Existing context menu modifications should apply to new private windows when +// private browsing support is enabled unless unloaded. +exports.testNewPrivateEnabledWindowUnloaded = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newPrivateLoader(); + + let item = new loader.cm.Item({ label: "item" }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + popup.hidePopup(); + + loader.unload(); + + test.withNewPrivateWindow(function () { + test.showMenu(null, function (popup) { + test.checkMenu([], [], []); + test.done(); + }); + }); + }); +}; + + +// Items in the context menu should be sorted according to locale. +exports.testSorting = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + // Make an unsorted items list. It'll look like this: + // item 1, item 0, item 3, item 2, item 5, item 4, ... + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i += 2) { + items.push(new loader.cm.Item({ label: "item " + (i + 1) })); + items.push(new loader.cm.Item({ label: "item " + i })); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Items in the overflow menu should be sorted according to locale. +exports.testSortingOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + // Make an unsorted items list. It'll look like this: + // item 1, item 0, item 3, item 2, item 5, item 4, ... + let items = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT * 2; i += 2) { + items.push(new loader.cm.Item({ label: "item " + (i + 1) })); + items.push(new loader.cm.Item({ label: "item " + i })); + } + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Multiple modules shouldn't interfere with sorting. +exports.testSortingMultipleModules = function (assert, done) { + let test = new TestHelper(assert, done); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let items0 = []; + let items1 = []; + for (let i = 0; i < OVERFLOW_THRESH_DEFAULT; i++) { + if (i % 2) { + let item = new loader0.cm.Item({ label: "item " + i }); + items0.push(item); + } + else { + let item = new loader1.cm.Item({ label: "item " + i }); + items1.push(item); + } + } + let allItems = items0.concat(items1); + + test.showMenu(null, function (popup) { + + // All items should be present and sorted. + test.checkMenu(allItems, [], []); + popup.hidePopup(); + loader0.unload(); + loader1.unload(); + test.showMenu(null, function (popup) { + + // All items should be removed. + test.checkMenu(allItems, [], allItems); + test.done(); + }); + }); +}; + + +// Content click handlers and context handlers should be able to communicate, +// i.e., they're eval'ed in the same worker and sandbox. +exports.testContentCommunication = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ + label: "item", + contentScript: 'var potato;' + + 'self.on("context", function () {' + + ' potato = "potato";' + + ' return true;' + + '});' + + 'self.on("click", function () {' + + ' self.postMessage(potato);' + + '});', + }); + + item.on("message", function (data) { + assert.equal(data, "potato", "That's a lot of potatoes!"); + test.done(); + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + let elt = test.getItemElt(popup, item); + elt.click(); + }); +}; + + +// When the context menu is invoked on a tab that was already open when the +// module was loaded, it should contain the expected items and content workers +// should function as expected. +exports.testLoadWithOpenTab = function (assert, done) { + let test = new TestHelper(assert, done); + test.withTestDoc(function (window, doc) { + let loader = test.newLoader(); + let item = new loader.cm.Item({ + label: "item", + contentScript: + 'self.on("click", () => self.postMessage("click"));', + onMessage: function (msg) { + if (msg === "click") + test.done(); + } + }); + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.getItemElt(popup, item).click(); + }); + }); +}; + +// Bug 732716: Ensure that the node given in `click` event works fine +// (i.e. is correctly wrapped) +exports.testDrawImageOnClickNode = function (assert, done) { + let test = new TestHelper(assert, done); + test.withTestDoc(function (window, doc) { + let loader = test.newLoader(); + let item = new loader.cm.Item({ + label: "item", + context: loader.cm.SelectorContext("img"), + contentScript: "new " + function() { + self.on("click", function (img, data) { + let ctx = document.createElement("canvas").getContext("2d"); + ctx.drawImage(img, 1, 1, 1, 1); + self.postMessage("done"); + }); + }, + onMessage: function (msg) { + if (msg === "done") + test.done(); + } + }); + test.showMenu("#image", function (popup) { + test.checkMenu([item], [], []); + test.getItemElt(popup, item).click(); + }); + }); +}; + + +// Setting an item's label before the menu is ever shown should correctly change +// its label. +exports.testSetLabelBeforeShow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ] + items[0].label = "z"; + assert.equal(items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Setting an item's label after the menu is shown should correctly change its +// label. +exports.testSetLabelAfterShow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + popup.hidePopup(); + + items[0].label = "z"; + assert.equal(items[0].label, "z"); + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Setting an item's label before the menu is ever shown should correctly change +// its label. +exports.testSetLabelBeforeShowOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let prefs = loader.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ] + items[0].label = "z"; + assert.equal(items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); +}; + + +// Setting an item's label after the menu is shown should correctly change its +// label. +exports.testSetLabelAfterShowOverflow = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let prefs = loader.loader.require("sdk/preferences/service"); + prefs.set(OVERFLOW_THRESH_PREF, 0); + + let items = [ + new loader.cm.Item({ label: "a" }), + new loader.cm.Item({ label: "b" }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + popup.hidePopup(); + + items[0].label = "z"; + assert.equal(items[0].label, "z"); + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Setting the label of an item in a Menu should work. +exports.testSetLabelMenuItem = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [loader.cm.Item({ label: "a" })] + }); + menu.items[0].label = "z"; + + assert.equal(menu.items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Menu.addItem() should work. +exports.testMenuAddItem = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }) + ] + }); + menu.addItem(loader.cm.Item({ label: "item 1" })); + menu.addItem(loader.cm.Item({ label: "item 2" })); + + assert.equal(menu.items.length, 3, + "menu should have correct number of items"); + for (let i = 0; i < 3; i++) { + assert.equal(menu.items[i].label, "item " + i, + "item label should be correct"); + assert.equal(menu.items[i].parentMenu, menu, + "item's parent menu should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Adding the same item twice to a menu should work as expected. +exports.testMenuAddItemTwice = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [] + }); + let subitem = loader.cm.Item({ label: "item 1" }) + menu.addItem(subitem); + menu.addItem(loader.cm.Item({ label: "item 0" })); + menu.addItem(subitem); + + assert.equal(menu.items.length, 2, + "menu should have correct number of items"); + for (let i = 0; i < 2; i++) { + assert.equal(menu.items[i].label, "item " + i, + "item label should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Menu.removeItem() should work. +exports.testMenuRemoveItem = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item 1" }); + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }), + subitem, + loader.cm.Item({ label: "item 2" }) + ] + }); + + // Removing twice should be harmless. + menu.removeItem(subitem); + menu.removeItem(subitem); + + assert.equal(subitem.parentMenu, null, + "item's parent menu should be correct"); + + assert.equal(menu.items.length, 2, + "menu should have correct number of items"); + assert.equal(menu.items[0].label, "item 0", + "item label should be correct"); + assert.equal(menu.items[1].label, "item 2", + "item label should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Adding an item currently contained in one menu to another menu should work. +exports.testMenuItemSwap = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item" }); + let menu0 = loader.cm.Menu({ + label: "menu 0", + items: [subitem] + }); + let menu1 = loader.cm.Menu({ + label: "menu 1", + items: [] + }); + menu1.addItem(subitem); + + assert.equal(menu0.items.length, 0, + "menu should have correct number of items"); + + assert.equal(menu1.items.length, 1, + "menu should have correct number of items"); + assert.equal(menu1.items[0].label, "item", + "item label should be correct"); + + assert.equal(subitem.parentMenu, menu1, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu0, menu1], [menu0], []); + test.done(); + }); +}; + + +// Destroying an item should remove it from its parent menu. +exports.testMenuItemDestroy = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item" }); + let menu = loader.cm.Menu({ + label: "menu", + items: [subitem] + }); + subitem.destroy(); + + assert.equal(menu.items.length, 0, + "menu should have correct number of items"); + assert.equal(subitem.parentMenu, null, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [menu], []); + test.done(); + }); +}; + + +// Setting Menu.items should work. +exports.testMenuItemsSetter = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "old item 0" }), + loader.cm.Item({ label: "old item 1" }) + ] + }); + menu.items = [ + loader.cm.Item({ label: "new item 0" }), + loader.cm.Item({ label: "new item 1" }), + loader.cm.Item({ label: "new item 2" }) + ]; + + assert.equal(menu.items.length, 3, + "menu should have correct number of items"); + for (let i = 0; i < 3; i++) { + assert.equal(menu.items[i].label, "new item " + i, + "item label should be correct"); + assert.equal(menu.items[i].parentMenu, menu, + "item's parent menu should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Setting Item.data should work. +exports.testItemDataSetter = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = loader.cm.Item({ label: "old item 0", data: "old" }); + item.data = "new"; + + assert.equal(item.data, "new", "item should have correct data"); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + +// Open the test doc, load the module, make sure items appear when context- +// clicking the iframe. +exports.testAlreadyOpenIframe = function (assert, done) { + let test = new TestHelper(assert, done); + test.withTestDoc(function (window, doc) { + let loader = test.newLoader(); + let item = new loader.cm.Item({ + label: "item" + }); + test.showMenu("#iframe", function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); + }); +}; + + +// Tests that a missing label throws an exception +exports.testItemNoLabel = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + try { + new loader.cm.Item({}); + assert.ok(false, "Should have seen exception"); + } + catch (e) { + assert.ok(true, "Should have seen exception"); + } + + try { + new loader.cm.Item({ label: null }); + assert.ok(false, "Should have seen exception"); + } + catch (e) { + assert.ok(true, "Should have seen exception"); + } + + try { + new loader.cm.Item({ label: undefined }); + assert.ok(false, "Should have seen exception"); + } + catch (e) { + assert.ok(true, "Should have seen exception"); + } + + try { + new loader.cm.Item({ label: "" }); + assert.ok(false, "Should have seen exception"); + } + catch (e) { + assert.ok(true, "Should have seen exception"); + } + + test.done(); +} + +/* bug 1302854 - disabled this subtest as it is intermittent +// Tests that items can have an empty data property +exports.testItemNoData = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + function checkData(data) { + assert.equal(data, undefined, "Data should be undefined"); + } + + let item1 = new loader.cm.Item({ + label: "item 1", + contentScript: 'self.on("click", (node, data) => self.postMessage(data))', + onMessage: checkData + }); + let item2 = new loader.cm.Item({ + label: "item 2", + data: null, + contentScript: 'self.on("click", (node, data) => self.postMessage(data))', + onMessage: checkData + }); + let item3 = new loader.cm.Item({ + label: "item 3", + data: undefined, + contentScript: 'self.on("click", (node, data) => self.postMessage(data))', + onMessage: checkData + }); + + assert.equal(item1.data, undefined, "Should be no defined data"); + assert.equal(item2.data, null, "Should be no defined data"); + assert.equal(item3.data, undefined, "Should be no defined data"); + + test.showMenu(null, function (popup) { + test.checkMenu([item1, item2, item3], [], []); + + let itemElt = test.getItemElt(popup, item1); + itemElt.click(); + + test.hideMenu(function() { + test.showMenu(null, function (popup) { + let itemElt = test.getItemElt(popup, item2); + itemElt.click(); + + test.hideMenu(function() { + test.showMenu(null, function (popup) { + let itemElt = test.getItemElt(popup, item3); + itemElt.click(); + + test.done(); + }); + }); + }); + }); + }); +} +*/ + + +exports.testItemNoAccessKey = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item1 = new loader.cm.Item({ label: "item 1" }); + let item2 = new loader.cm.Item({ label: "item 2", accesskey: null }); + let item3 = new loader.cm.Item({ label: "item 3", accesskey: undefined }); + + assert.equal(item1.accesskey, undefined, "Should be no defined image"); + assert.equal(item2.accesskey, null, "Should be no defined image"); + assert.equal(item3.accesskey, undefined, "Should be no defined image"); + + test.showMenu(). + then((popup) => test.checkMenu([item1, item2, item3], [], [])). + then(test.done). + catch(assert.fail); +} + + +// Test accesskey support. +exports.testItemAccessKey = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item = new loader.cm.Item({ label: "item", accesskey: "i" }); + assert.equal(item.accesskey, "i", "Should have set the image to i"); + + let menu = new loader.cm.Menu({ label: "menu", accesskey: "m", items: [ + loader.cm.Item({ label: "subitem" }) + ]}); + assert.equal(menu.accesskey, "m", "Should have set the accesskey to m"); + + test.showMenu().then((popup) => { + test.checkMenu([item, menu], [], []); + + let accesskey = "e"; + menu.accesskey = item.accesskey = accesskey; + assert.equal(item.accesskey, accesskey, "Should have set the accesskey to " + accesskey); + assert.equal(menu.accesskey, accesskey, "Should have set the accesskey to " + accesskey); + test.checkMenu([item, menu], [], []); + + item.accesskey = null; + menu.accesskey = null; + assert.equal(item.accesskey, null, "Should have set the accesskey to " + accesskey); + assert.equal(menu.accesskey, null, "Should have set the accesskey to " + accesskey); + test.checkMenu([item, menu], [], []); + }). + then(test.done). + catch(assert.fail); +}; + + +// Tests that items without an image don't attempt to show one +exports.testItemNoImage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let item1 = new loader.cm.Item({ label: "item 1" }); + let item2 = new loader.cm.Item({ label: "item 2", image: null }); + let item3 = new loader.cm.Item({ label: "item 3", image: undefined }); + + assert.equal(item1.image, undefined, "Should be no defined image"); + assert.equal(item2.image, null, "Should be no defined image"); + assert.equal(item3.image, undefined, "Should be no defined image"); + + test.showMenu(null, function (popup) { + test.checkMenu([item1, item2, item3], [], []); + + test.done(); + }); +} + + +// Test image support. +exports.testItemImage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let imageURL = data.url("moz_favicon.ico"); + let item = new loader.cm.Item({ label: "item", image: imageURL }); + let menu = new loader.cm.Menu({ label: "menu", image: imageURL, items: [ + loader.cm.Item({ label: "subitem" }) + ]}); + assert.equal(item.image, imageURL, "Should have set the image correctly"); + assert.equal(menu.image, imageURL, "Should have set the image correctly"); + + test.showMenu(null, function (popup) { + test.checkMenu([item, menu], [], []); + + let imageURL2 = data.url("dummy.ico"); + item.image = imageURL2; + menu.image = imageURL2; + assert.equal(item.image, imageURL2, "Should have set the image correctly"); + assert.equal(menu.image, imageURL2, "Should have set the image correctly"); + test.checkMenu([item, menu], [], []); + + item.image = null; + menu.image = null; + assert.equal(item.image, null, "Should have set the image correctly"); + assert.equal(menu.image, null, "Should have set the image correctly"); + test.checkMenu([item, menu], [], []); + + test.done(); + }); +}; + +// Test image URL validation. +exports.testItemImageValidURL = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + assert.throws(function(){ + new loader.cm.Item({ + label: "item 1", + image: "foo" + }) + }, /Image URL validation failed/ + ); + + assert.throws(function(){ + new loader.cm.Item({ + label: "item 2", + image: false + }) + }, /Image URL validation failed/ + ); + + assert.throws(function(){ + new loader.cm.Item({ + label: "item 3", + image: 0 + }) + }, /Image URL validation failed/ + ); + + let imageURL = data.url("moz_favicon.ico"); + let item4 = new loader.cm.Item({ label: "item 4", image: imageURL }); + let item5 = new loader.cm.Item({ label: "item 5", image: null }); + let item6 = new loader.cm.Item({ label: "item 6", image: undefined }); + + assert.equal(item4.image, imageURL, "Should be proper image URL"); + assert.equal(item5.image, null, "Should be null image"); + assert.equal(item6.image, undefined, "Should be undefined image"); + + test.done(); +}; + + +// Menu.destroy should destroy the item tree rooted at that menu. +exports.testMenuDestroy = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }), + loader.cm.Menu({ + label: "item 1", + items: [ + loader.cm.Item({ label: "subitem 0" }), + loader.cm.Item({ label: "subitem 1" }), + loader.cm.Item({ label: "subitem 2" }) + ] + }), + loader.cm.Item({ label: "item 2" }) + ] + }); + menu.destroy(); + + /*let numRegistryEntries = 0; + loader.globalScope.browserManager.browserWins.forEach(function (bwin) { + for (let itemID in bwin.items) + numRegistryEntries++; + }); + assert.equal(numRegistryEntries, 0, "All items should be unregistered.");*/ + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], [menu]); + test.done(); + }); +}; + +// Checks that if a menu contains sub items that are hidden then the menu is +// hidden too. Also checks that content scripts and contexts work for sub items. +exports.testSubItemContextNoMatchHideMenu = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + loader.cm.Menu({ + label: "menu 1", + items: [ + loader.cm.Item({ + label: "subitem 1", + context: loader.cm.SelectorContext(".foo") + }) + ] + }), + loader.cm.Menu({ + label: "menu 2", + items: [ + loader.cm.Item({ + label: "subitem 2", + contentScript: 'self.on("context", () => false);' + }) + ] + }), + loader.cm.Menu({ + label: "menu 3", + items: [ + loader.cm.Item({ + label: "subitem 3", + context: loader.cm.SelectorContext(".foo") + }), + loader.cm.Item({ + label: "subitem 4", + contentScript: 'self.on("context", () => false);' + }) + ] + }) + ]; + + test.showMenu(null, function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); +}; + + +// Checks that if a menu contains a combination of hidden and visible sub items +// then the menu is still visible too. +exports.testSubItemContextMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let hiddenItems = [ + loader.cm.Item({ + label: "subitem 3", + context: loader.cm.SelectorContext(".foo") + }), + loader.cm.Item({ + label: "subitem 6", + contentScript: 'self.on("context", () => false);' + }) + ]; + + let items = [ + loader.cm.Menu({ + label: "menu 1", + items: [ + loader.cm.Item({ + label: "subitem 1", + context: loader.cm.URLContext(TEST_DOC_URL) + }) + ] + }), + loader.cm.Menu({ + label: "menu 2", + items: [ + loader.cm.Item({ + label: "subitem 2", + contentScript: 'self.on("context", () => true);' + }) + ] + }), + loader.cm.Menu({ + label: "menu 3", + items: [ + hiddenItems[0], + loader.cm.Item({ + label: "subitem 4", + contentScript: 'self.on("context", () => true);' + }) + ] + }), + loader.cm.Menu({ + label: "menu 4", + items: [ + loader.cm.Item({ + label: "subitem 5", + context: loader.cm.URLContext(TEST_DOC_URL) + }), + hiddenItems[1] + ] + }), + loader.cm.Menu({ + label: "menu 5", + items: [ + loader.cm.Item({ + label: "subitem 7", + context: loader.cm.URLContext(TEST_DOC_URL) + }), + loader.cm.Item({ + label: "subitem 8", + contentScript: 'self.on("context", () => true);' + }) + ] + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, hiddenItems, []); + test.done(); + }); + }); +}; + + +// Child items should default to visible, not to PageContext +exports.testSubItemDefaultVisible = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [ + loader.cm.Menu({ + label: "menu 1", + context: loader.cm.SelectorContext("img"), + items: [ + loader.cm.Item({ + label: "subitem 1" + }), + loader.cm.Item({ + label: "subitem 2", + context: loader.cm.SelectorContext("img") + }), + loader.cm.Item({ + label: "subitem 3", + context: loader.cm.SelectorContext("a") + }) + ] + }) + ]; + + // subitem 3 will be hidden + let hiddenItems = [items[0].items[2]]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, hiddenItems, []); + test.done(); + }); + }); +}; + +// Tests that the click event on sub menuitem +// tiggers the click event for the sub menuitem and the parent menu +exports.testSubItemClick = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let state = 0; + + let items = [ + loader.cm.Menu({ + label: "menu 1", + items: [ + loader.cm.Item({ + label: "subitem 1", + data: "foobar", + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function(msg) { + assert.equal(msg.tagName, "HTML", "should have seen the right node"); + assert.equal(msg.data, "foobar", "should have seen the right data"); + assert.equal(state, 0, "should have seen the event at the right time"); + state++; + } + }) + ], + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function(msg) { + assert.equal(msg.tagName, "HTML", "should have seen the right node"); + assert.equal(msg.data, "foobar", "should have seen the right data"); + assert.equal(state, 1, "should have seen the event at the right time"); + + test.done(); + } + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + + let topMenuElt = test.getItemElt(popup, items[0]); + let topMenuPopup = topMenuElt.firstChild; + let itemElt = test.getItemElt(topMenuPopup, items[0].items[0]); + itemElt.click(); + }); + }); +}; + +// Tests that the command event on sub menuitem +// tiggers the click event for the sub menuitem and the parent menu +exports.testSubItemCommand = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let state = 0; + + let items = [ + loader.cm.Menu({ + label: "menu 1", + items: [ + loader.cm.Item({ + label: "subitem 1", + data: "foobar", + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function(msg) { + assert.equal(msg.tagName, "HTML", "should have seen the right node"); + assert.equal(msg.data, "foobar", "should have seen the right data"); + assert.equal(state, 0, "should have seen the event at the right time"); + state++; + } + }) + ], + contentScript: 'self.on("click", function (node, data) {' + + ' self.postMessage({' + + ' tagName: node.tagName,' + + ' data: data' + + ' });' + + '});', + onMessage: function(msg) { + assert.equal(msg.tagName, "HTML", "should have seen the right node"); + assert.equal(msg.data, "foobar", "should have seen the right data"); + assert.equal(state, 1, "should have seen the event at the right time"); + state++ + + test.done(); + } + }) + ]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + + let topMenuElt = test.getItemElt(popup, items[0]); + let topMenuPopup = topMenuElt.firstChild; + let itemElt = test.getItemElt(topMenuPopup, items[0].items[0]); + + // create a command event + let evt = itemElt.ownerDocument.createEvent('Event'); + evt.initEvent('command', true, true); + itemElt.dispatchEvent(evt); + }); + }); +}; + +// Tests that opening a context menu for an outer frame when an inner frame +// has a selection doesn't activate the SelectionContext +exports.testSelectionInInnerFrameNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let state = 0; + + let items = [ + loader.cm.Item({ + label: "test item", + context: loader.cm.SelectionContext() + }) + ]; + + test.withTestDoc(function (window, doc) { + let frame = doc.getElementById("iframe"); + frame.contentWindow.getSelection().selectAllChildren(frame.contentDocument.body); + + test.showMenu(null, function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + +// Tests that opening a context menu for an inner frame when the inner frame +// has a selection does activate the SelectionContext +exports.testSelectionInInnerFrameMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let state = 0; + + let items = [ + loader.cm.Item({ + label: "test item", + context: loader.cm.SelectionContext() + }) + ]; + + test.withTestDoc(function (window, doc) { + let frame = doc.getElementById("iframe"); + frame.contentWindow.getSelection().selectAllChildren(frame.contentDocument.body); + + test.showMenu(["#iframe", "#text"], function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Tests that opening a context menu for an inner frame when the outer frame +// has a selection doesn't activate the SelectionContext +exports.testSelectionInOuterFrameNoMatch = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let state = 0; + + let items = [ + loader.cm.Item({ + label: "test item", + context: loader.cm.SelectionContext() + }) + ]; + + test.withTestDoc(function (window, doc) { + let frame = doc.getElementById("iframe"); + window.getSelection().selectAllChildren(doc.body); + + test.showMenu(["#iframe", "#text"], function (popup) { + test.checkMenu(items, items, []); + test.done(); + }); + }); +}; + + +// Test that the return value of the predicate function determines if +// item is shown +exports.testPredicateContextControl = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let itemTrue = loader.cm.Item({ + label: "visible", + context: loader.cm.PredicateContext(function () { return true; }) + }); + + let itemFalse = loader.cm.Item({ + label: "hidden", + context: loader.cm.PredicateContext(function () { return false; }) + }); + + test.showMenu(null, function (popup) { + test.checkMenu([itemTrue, itemFalse], [itemFalse], []); + test.done(); + }); +}; + +// Test that the data object has the correct document type +exports.testPredicateContextDocumentType = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.equal(data.documentType, 'text/html'); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct document URL +exports.testPredicateContextDocumentURL = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.equal(data.documentURL, TEST_DOC_URL); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object has the correct element name +exports.testPredicateContextTargetName = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.targetName, "input"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#button", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object has the correct ID +exports.testPredicateContextTargetIDSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.targetID, "button"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#button", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct ID +exports.testPredicateContextTargetIDNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.targetID, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(".predicate-test-a", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for regular text inputs +exports.testPredicateContextTextBoxIsEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, true); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#textbox", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for readonly text inputs +exports.testPredicateContextReadonlyTextBoxIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#readonly-textbox", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for disabled text inputs +exports.testPredicateContextDisabledTextBoxIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#disabled-textbox", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for text areas +exports.testPredicateContextTextAreaIsEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, true); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#textfield", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that non-text inputs are not considered editable +exports.testPredicateContextButtonIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#button", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object is showing editable correctly +exports.testPredicateContextNonInputIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object is showing editable correctly for HTML contenteditable elements +exports.testPredicateContextEditableElement = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, true); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#editable", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object does not have a selection when there is none +exports.testPredicateContextNoSelectionInPage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.selectionText, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object includes the selected page text +exports.testPredicateContextSelectionInPage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + // since we might get whitespace + assert.ok(data.selectionText && data.selectionText.search(/^\s*Some text.\s*$/) != -1, + 'Expected "Some text.", got "' + data.selectionText + '"'); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + window.getSelection().selectAllChildren(doc.getElementById("text")); + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object includes the selected input text +exports.testPredicateContextSelectionInTextBox = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + // since we might get whitespace + assert.strictEqual(data.selectionText, "t v"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + let textbox = doc.getElementById("textbox"); + test.selectRange("#textbox", 3, 6); + test.showMenu("#textbox", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct src for an image +exports.testPredicateContextTargetSrcSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let image; + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.srcURL, image.src); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + image = doc.getElementById("image"); + test.showMenu("#image", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has no src for a link +exports.testPredicateContextTargetSrcNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.srcURL, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#link", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object has the correct link set +exports.testPredicateContextTargetLinkSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let image; + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.linkURL, TEST_DOC_URL + "#test"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(".predicate-test-a", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has no link for an image +exports.testPredicateContextTargetLinkNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.linkURL, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct link for a nested image +exports.testPredicateContextTargetLinkSetNestedImage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.linkURL, TEST_DOC_URL + "#nested-image"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#predicate-test-nested-image", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct link for a complex nested structure +exports.testPredicateContextTargetLinkSetNestedStructure = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.linkURL, TEST_DOC_URL + "#nested-structure"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#predicate-test-nested-structure", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the value for an input textbox +exports.testPredicateContextTargetValueSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let image; + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.value, "test value"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#textbox", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has no value for an image +exports.testPredicateContextTargetValueNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.value, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu("#image", function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +if (isTravisCI) { + 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-context-menu@2.js b/addon-sdk/source/test/test-context-menu@2.js new file mode 100644 index 000000000..78c496220 --- /dev/null +++ b/addon-sdk/source/test/test-context-menu@2.js @@ -0,0 +1,1350 @@ +/* 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, Ci } = require("chrome"); +const {openWindow, closeWindow, openTab, closeTab, + openContextMenu, closeContextMenu, select, + readNode, captureContextMenu, withTab, withItems } = require("./context-menu/util"); +const {when} = require("sdk/dom/events"); +const {Item, Menu, Separator, Contexts, Readers } = require("sdk/context-menu@2"); +const prefs = require("sdk/preferences/service"); +const { before, after } = require('sdk/test/utils'); + +const testPageURI = require.resolve("./test-context-menu").replace(".js", ".html"); + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +const data = input => + `data:text/html;charset=utf-8,${encodeURIComponent(input)}` + +const menugroup = (...children) => Object.assign({ + tagName: "menugroup", + namespaceURI: XUL_NS, + style: "-moz-box-orient: vertical;", + className: "sdk-context-menu-extension" +}, children.length ? {children} : {}); + +const menuseparator = () => ({ + tagName: "menuseparator", + namespaceURI: XUL_NS, + className: "sdk-context-menu-separator" +}) + +const menuitem = properties => Object.assign({ + tagName: "menuitem", + namespaceURI: XUL_NS, + className: "sdk-context-menu-item menuitem-iconic" +}, properties); + +const menu = (properties, ...children) => Object.assign({ + tagName: "menu", + namespaceURI: XUL_NS, + className: "sdk-context-menu menu-iconic" +}, properties, { + children: [Object.assign({tagName: "menupopup", namespaceURI: XUL_NS}, + children.length ? {children} : {})] +}); + +// Destroying items that were previously created should cause them to be absent +// from the menu. +exports["test create / destroy menu item"] = withTab(function*(assert) { + const item = new Item({ + label: "test-1" + }); + + const before = yield captureContextMenu("h1"); + + assert.deepEqual(before, + menugroup(menuseparator(), + menuitem({label: "test-1"})), + "context menu contains separator & added item"); + + item.destroy(); + + const after = yield captureContextMenu("h1"); + assert.deepEqual(after, menugroup(), + "all items were removed children are present"); +}, data`

hello

`); + + +/* Bug 1115419 - Disable occasionally failing test until we + figure out why it fails. +// Items created should be present on all browser windows. +exports["test menu item in new window"] = function*(assert) { + const isMenuPopulated = function*(tab) { + const state = yield captureContextMenu("h1", tab); + assert.deepEqual(state, + menugroup(menuseparator(), + menuitem({label: "multi-window"})), + "created menu item is present") + }; + + const isMenuEmpty = function*(tab) { + const state = yield captureContextMenu("h1", tab); + assert.deepEqual(state, menugroup(), "no sdk items present"); + }; + + const item = new Item({ label: "multi-window" }); + + const tab1 = yield openTab(`data:text/html,

hello

`); + yield* isMenuPopulated(tab1); + + const window2 = yield openWindow(); + assert.pass("window is ready"); + + const tab2 = yield openTab(`data:text/html,

hello window-2

`, window2); + assert.pass("tab is ready"); + + yield* isMenuPopulated(tab2); + + item.destroy(); + + yield* isMenuEmpty(tab2); + yield closeWindow(window2); + + yield* isMenuEmpty(tab1); + + yield closeTab(tab1); +}; +*/ + + +// Multilpe items can be created and destroyed at different points +// in time & they should not affect each other. +exports["test multiple items"] = withTab(function*(assert) { + const item1 = new Item({ label: "one" }); + + const step1 = yield captureContextMenu("h1"); + assert.deepEqual(step1, + menugroup(menuseparator(), + menuitem({label: "one"})), + "item1 is present"); + + const item2 = new Item({ label: "two" }); + const step2 = yield captureContextMenu("h1"); + + assert.deepEqual(step2, + menugroup(menuseparator(), + menuitem({label: "one"}), + menuitem({label: "two"})), + "both items where present"); + + item1.destroy(); + + const step3 = yield captureContextMenu("h1"); + assert.deepEqual(step3, + menugroup(menuseparator(), + menuitem({label: "two"})), + "one items left"); + + item2.destroy(); + + const step4 = yield captureContextMenu("h1"); + assert.deepEqual(step4, menugroup(), "no items left"); +}, data`

Multiple Items

`); + +// Destroying an item twice should not cause an error. +exports["test destroy twice"] = withTab(function*(assert) { + const item = new Item({ label: "destroy" }); + const withItem = yield captureContextMenu("h2"); + assert.deepEqual(withItem, + menugroup(menuseparator(), + menuitem({label:"destroy"})), + "Item is added"); + + item.destroy(); + + const withoutItem = yield captureContextMenu("h2"); + assert.deepEqual(withoutItem, menugroup(), "Item was removed"); + + item.destroy(); + assert.pass("Destroying an item twice should not cause an error."); +}, "data:text/html,

item destroy

"); + +// CSS selector contexts should cause their items to be absent from the menu +// when the menu is not invoked on nodes that match selectors. +exports["test selector context"] = withTab(function*(assert) { + const item = new Item({ + context: [new Contexts.Selector("body b")], + label: "bold" + }); + + const match = yield captureContextMenu("b"); + assert.deepEqual(match, + menugroup(menuseparator(), + menuitem({label: "bold"})), + "item mathched context"); + + const noMatch = yield captureContextMenu("i"); + assert.deepEqual(noMatch, menugroup(), "item did not match context"); + + item.destroy(); + + const cleared = yield captureContextMenu("b"); + assert.deepEqual(cleared, menugroup(), "item was removed"); +}, data`onetwo`); + +// CSS selector contexts should cause their items to be absent in the menu +// when the menu is invoked even on nodes that have ancestors that match the +// selectors. +exports["test parent selector don't match children"] = withTab(function*(assert) { + const item = new Item({ + label: "parent match", + context: [new Contexts.Selector("a[href]")] + }); + + const match = yield captureContextMenu("a"); + assert.deepEqual(match, + menugroup(menuseparator(), + menuitem({label: "parent match"})), + "item mathched context"); + + const noMatch = yield captureContextMenu("strong"); + assert.deepEqual(noMatch, menugroup(), "item did not mathch context"); + + item.destroy(); + + const destroyed = yield captureContextMenu("a"); + assert.deepEqual(destroyed, menugroup(), "no items left"); +}, data`This text must be long & bold!`); + +// Page contexts should cause their items to be present in the menu when the +// menu is not invoked on an active element. +exports["test page context match"] = withTab(function*(assert) { + const isPageMatch = (tree, description="page context matched") => + assert.deepEqual(tree, + menugroup(menuseparator(), + menuitem({label: "page match"}), + menuitem({label: "any match"})), + description); + + const isntPageMatch = (tree, description="page context did not match") => + assert.deepEqual(tree, + menugroup(menuseparator(), + menuitem({label: "any match"})), + description); + + yield* withItems({ + pageMatch: new Item({ + label: "page match", + context: [new Contexts.Page()], + }), + anyMatch: new Item({ + label: "any match" + }) + }, function*({pageMatch, anyMatch}) { + for (let tagName of [null, "p", "h3"]) { + isPageMatch((yield captureContextMenu(tagName)), + `Page context matches ${tagName} passive element`); + } + + for (let tagName of ["button", "canvas", "img", "input", "textarea", + "select", "menu", "embed" ,"object", "video", "audio", + "applet"]) + { + isntPageMatch((yield captureContextMenu(tagName)), + `Page context does not match <${tagName}/> active element`); + } + + for (let selector of ["span"]) + { + isntPageMatch((yield captureContextMenu(selector)), + `Page context does not match decedents of active element`); + } + }); +}, +data` + + + +

paragraph

+ +

hi

+
+
+
+
+
+
+
+
+
+
+
+
+`); + +// Page context does not match if if there is a selection. +exports["test page context doesn't match on selection"] = withTab(function*(assert) { + const isPageMatch = (tree, description="page context matched") => + assert.deepEqual(tree, + menugroup(menuseparator(), + menuitem({label: "page match"}), + menuitem({label: "any match"})), + description); + + const isntPageMatch = (tree, description="page context did not match") => + assert.deepEqual(tree, + menugroup(menuseparator(), + menuitem({label: "any match"})), + description); + + yield* withItems({ + pageMatch: new Item({ + label: "page match", + context: [new Contexts.Page()], + }), + anyMatch: new Item({ + label: "any match" + }) + }, function*({pageMatch, anyMatch}) { + yield select("b"); + isntPageMatch((yield captureContextMenu("i")), + "page context does not match if there is a selection"); + + yield select(null); + isPageMatch((yield captureContextMenu("i")), + "page context match if there is no selection"); + }); +}, data`onetwo`); + +exports["test selection context"] = withTab(function*(assert) { + yield* withItems({ + item: new Item({ + label: "selection", + context: [new Contexts.Selection()] + }) + }, function*({item}) { + assert.deepEqual((yield captureContextMenu()), + menugroup(), + "item does not match if there is no selection"); + + yield select("b"); + + assert.deepEqual((yield captureContextMenu()), + menugroup(menuseparator(), + menuitem({label: "selection"})), + "item matches if there is a selection"); + }); +}, data`onetwo`); + +exports["test selection context in textarea"] = withTab(function*(assert) { + yield* withItems({ + item: new Item({ + label: "selection", + context: [new Contexts.Selection()] + }) + }, function*({item}) { + assert.deepEqual((yield captureContextMenu()), + menugroup(), + "does not match if there's no selection"); + + yield select({target:"textarea", start:0, end:5}); + + assert.deepEqual((yield captureContextMenu("b")), + menugroup(), + "does not match if target isn't input with selection"); + + assert.deepEqual((yield captureContextMenu("textarea")), + menugroup(menuseparator(), + menuitem({label: "selection"})), + "matches if target is input with selected text"); + + yield select({target: "textarea", start: 0, end: 0}); + + assert.deepEqual((yield captureContextMenu("textarea")), + menugroup(), + "does not match when selection is cleared"); + }); +}, data`!!`); + +exports["test url contexts"] = withTab(function*(assert) { + yield* withItems({ + a: new Item({ + label: "a", + context: [new Contexts.URL(testPageURI)] + }), + b: new Item({ + label: "b", + context: [new Contexts.URL("*.bogus.com")] + }), + c: new Item({ + label: "c", + context: [new Contexts.URL("*.bogus.com"), + new Contexts.URL(testPageURI)] + }), + d: new Item({ + label: "d", + context: [new Contexts.URL(/.*\.html/)] + }), + e: new Item({ + label: "e", + context: [new Contexts.URL("http://*"), + new Contexts.URL(testPageURI)] + }), + f: new Item({ + label: "f", + context: [new Contexts.URL("http://*").required, + new Contexts.URL(testPageURI)] + }), + }, function*(_) { + assert.deepEqual((yield captureContextMenu()), + menugroup(menuseparator(), + menuitem({label: "a"}), + menuitem({label: "c"}), + menuitem({label: "d"}), + menuitem({label: "e"})), + "shows only matching items"); + }); +}, testPageURI); + +exports["test iframe context"] = withTab(function*(assert) { + yield* withItems({ + page: new Item({ + label: "page", + context: [new Contexts.Page()] + }), + iframe: new Item({ + label: "iframe", + context: [new Contexts.Frame()] + }), + h2: new Item({ + label: "element", + context: [new Contexts.Selector("*")] + }) + }, function(_) { + assert.deepEqual((yield captureContextMenu("iframe")), + menugroup(menuseparator(), + menuitem({label: "page"}), + menuitem({label: "iframe"}), + menuitem({label: "element"})), + "matching items are present"); + + assert.deepEqual((yield captureContextMenu("h1")), + menugroup(menuseparator(), + menuitem({label: "page"}), + menuitem({label: "element"})), + "only matching items are present"); + + }); + +}, +data`

hello

+

+

+

+

+

+

+

+

+ +`; +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") + }; +} + +before(exports, (name, assert) => { + // Make sure Java doesn't activate + prefs.set("plugin.state.java", 0); +}); + +after(exports, (name, assert) => { + prefs.reset("plugin.state.java"); +}); + +require("sdk/test").run(module.exports); diff --git a/addon-sdk/source/test/test-cuddlefish.js b/addon-sdk/source/test/test-cuddlefish.js new file mode 100644 index 000000000..c92eaa624 --- /dev/null +++ b/addon-sdk/source/test/test-cuddlefish.js @@ -0,0 +1,78 @@ +/* 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, 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); + } + + let loader = Loader(override(packaging, { + globals: { + print: print, + foo: 1 + } + })); + let require = Require(loader, module); + + var fixture = require('./loader/fixture'); + + assert.equal(fixture.foo, 1, 'custom globals must work.'); + assert.equal(fixture.bar, 2, 'exports are set'); + + assert.equal(prints[0], 'testing', 'global print must be injected.'); + + var unloadsCalled = ''; + + require("sdk/system/unload").when(function(reason) { + assert.equal(reason, 'test', 'unload reason is passed'); + unloadsCalled += 'a'; + }); + require('sdk/system/unload.js').when(function() { + unloadsCalled += 'b'; + }); + + unload(loader, 'test'); + + assert.equal(unloadsCalled, 'ba', + 'loader.unload() must call listeners in LIFO order.'); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-deprecate.js b/addon-sdk/source/test/test-deprecate.js new file mode 100644 index 000000000..c1bd443c6 --- /dev/null +++ b/addon-sdk/source/test/test-deprecate.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 deprecate = require("sdk/util/deprecate"); +const { LoaderWithHookedConsole } = require("sdk/test/loader"); +const { get, set } = require("sdk/preferences/service"); +const PREFERENCE = "devtools.errorconsole.deprecation_warnings"; + +exports["test Deprecate Usage"] = function testDeprecateUsage(assert) { + set(PREFERENCE, true); + let { loader, messages } = LoaderWithHookedConsole(module); + let deprecate = loader.require("sdk/util/deprecate"); + + function functionIsDeprecated() { + deprecate.deprecateUsage("foo"); + } + + functionIsDeprecated(); + + 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; + + assert.ok(msg.indexOf("foo") !== -1, + "message contains the given message"); + assert.ok(msg.indexOf("functionIsDeprecated") !== -1, + "message contains name of the caller function"); + assert.ok(msg.indexOf(module.uri) !== -1, + "message contains URI of the caller module"); + + loader.unload(); +} + +exports["test Deprecate Function"] = function testDeprecateFunction(assert) { + set(PREFERENCE, true); + let { loader, messages } = LoaderWithHookedConsole(module); + let deprecate = loader.require("sdk/util/deprecate"); + + let self = {}; + let arg1 = "foo"; + let arg2 = {}; + + function originalFunction(a1, a2) { + assert.equal(this, self); + assert.equal(a1, arg1); + assert.equal(a2, arg2); + }; + + let deprecateFunction = deprecate.deprecateFunction(originalFunction, + "bar"); + + deprecateFunction.call(self, arg1, arg2); + + 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; + assert.ok(msg.indexOf("bar") !== -1, "message contains the given message"); + assert.ok(msg.indexOf("testDeprecateFunction") !== -1, + "message contains name of the caller function"); + assert.ok(msg.indexOf(module.uri) !== -1, + "message contains URI of the caller module"); + + loader.unload(); +} + +exports.testDeprecateEvent = function(assert, done) { + set(PREFERENCE, true); + let { loader, messages } = LoaderWithHookedConsole(module); + let deprecate = loader.require("sdk/util/deprecate"); + + let { on, emit } = loader.require('sdk/event/core'); + let testObj = {}; + testObj.on = deprecate.deprecateEvent(on.bind(null, testObj), 'BAD', ['fire']); + + testObj.on('fire', function() { + testObj.on('water', function() { + assert.equal(messages.length, 1, "only one error is dispatched"); + loader.unload(); + 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; + assert.ok(msg.indexOf("BAD") !== -1, "message contains the given message"); + assert.ok(msg.indexOf("deprecateEvent") !== -1, + "message contains name of the caller function"); + assert.ok(msg.indexOf(module.uri) !== -1, + "message contains URI of the caller module"); + + emit(testObj, 'fire'); +} + +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'); + + set(PREFERENCE, false); + fn(); + assert.equal(messages.length, 1, 'no new deprecation warnings'); +}; + +exports.testDeprecateSetting = function (assert, done) { + // Set devtools.errorconsole.deprecation_warnings to false + set(PREFERENCE, false); + + let { loader, messages } = LoaderWithHookedConsole(module); + let deprecate = loader.require("sdk/util/deprecate"); + + // deprecateUsage test + function functionIsDeprecated() { + deprecate.deprecateUsage("foo"); + } + functionIsDeprecated(); + + assert.equal(messages.length, 0, + "no errors dispatched on deprecateUsage"); + + // deprecateFunction test + function originalFunction() {}; + + let deprecateFunction = deprecate.deprecateFunction(originalFunction, + "bar"); + deprecateFunction(); + + assert.equal(messages.length, 0, + "no errors dispatched on deprecateFunction"); + + // deprecateEvent + let { on, emit } = loader.require('sdk/event/core'); + let testObj = {}; + testObj.on = deprecate.deprecateEvent(on.bind(null, testObj), 'BAD', ['fire']); + + testObj.on('fire', () => { + assert.equal(messages.length, 0, + "no errors dispatched on deprecateEvent"); + done(); + }); + + emit(testObj, 'fire'); +} + +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 new file mode 100644 index 000000000..fb786b043 --- /dev/null +++ b/addon-sdk/source/test/test-dev-panel.js @@ -0,0 +1,426 @@ +/* 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 = { + "engines": { + "Firefox": "*" + } +}; + +const { Tool } = require("dev/toolbox"); +const { Panel } = require("dev/panel"); +const { Class } = require("sdk/core/heritage"); +const { openToolbox, closeToolbox, getCurrentPanel } = require("dev/utils"); +const { MessageChannel } = require("sdk/messaging"); +const { when } = require("sdk/dom/events-shimmed"); +const { viewFor } = require("sdk/view/core"); +const { createView } = require("dev/panel/view"); + +const iconURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAAKQWlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N73QEiIgJfQaegkg0jtIFQRRiUmAUAKGhCZ2RAVGFBEpVmRUwAFHhyJjRRQLg4Ji1wnyEFDGwVFEReXdjGsJ7601896a/cdZ39nnt9fZZ+9917oAUPyCBMJ0WAGANKFYFO7rwVwSE8vE9wIYEAEOWAHA4WZmBEf4RALU/L09mZmoSMaz9u4ugGS72yy/UCZz1v9/kSI3QyQGAApF1TY8fiYX5QKUU7PFGTL/BMr0lSkyhjEyFqEJoqwi48SvbPan5iu7yZiXJuShGlnOGbw0noy7UN6aJeGjjAShXJgl4GejfAdlvVRJmgDl9yjT0/icTAAwFJlfzOcmoWyJMkUUGe6J8gIACJTEObxyDov5OWieAHimZ+SKBIlJYqYR15hp5ejIZvrxs1P5YjErlMNN4Yh4TM/0tAyOMBeAr2+WRQElWW2ZaJHtrRzt7VnW5mj5v9nfHn5T/T3IevtV8Sbsz55BjJ5Z32zsrC+9FgD2JFqbHbO+lVUAtG0GQOXhrE/vIADyBQC03pzzHoZsXpLE4gwnC4vs7GxzAZ9rLivoN/ufgm/Kv4Y595nL7vtWO6YXP4EjSRUzZUXlpqemS0TMzAwOl89k/fcQ/+PAOWnNycMsnJ/AF/GF6FVR6JQJhIlou4U8gViQLmQKhH/V4X8YNicHGX6daxRodV8AfYU5ULhJB8hvPQBDIwMkbj96An3rWxAxCsi+vGitka9zjzJ6/uf6Hwtcim7hTEEiU+b2DI9kciWiLBmj34RswQISkAd0oAo0gS4wAixgDRyAM3AD3iAAhIBIEAOWAy5IAmlABLJBPtgACkEx2AF2g2pwANSBetAEToI2cAZcBFfADXALDIBHQAqGwUswAd6BaQiC8BAVokGqkBakD5lC1hAbWgh5Q0FQOBQDxUOJkBCSQPnQJqgYKoOqoUNQPfQjdBq6CF2D+qAH0CA0Bv0BfYQRmALTYQ3YALaA2bA7HAhHwsvgRHgVnAcXwNvhSrgWPg63whfhG/AALIVfwpMIQMgIA9FGWAgb8URCkFgkAREha5EipAKpRZqQDqQbuY1IkXHkAwaHoWGYGBbGGeOHWYzhYlZh1mJKMNWYY5hWTBfmNmYQM4H5gqVi1bGmWCesP3YJNhGbjS3EVmCPYFuwl7ED2GHsOxwOx8AZ4hxwfrgYXDJuNa4Etw/XjLuA68MN4SbxeLwq3hTvgg/Bc/BifCG+Cn8cfx7fjx/GvyeQCVoEa4IPIZYgJGwkVBAaCOcI/YQRwjRRgahPdCKGEHnEXGIpsY7YQbxJHCZOkxRJhiQXUiQpmbSBVElqIl0mPSa9IZPJOmRHchhZQF5PriSfIF8lD5I/UJQoJhRPShxFQtlOOUq5QHlAeUOlUg2obtRYqpi6nVpPvUR9Sn0vR5Mzl/OX48mtk6uRa5Xrl3slT5TXl3eXXy6fJ18hf0r+pvy4AlHBQMFTgaOwVqFG4bTCPYVJRZqilWKIYppiiWKD4jXFUSW8koGStxJPqUDpsNIlpSEaQtOledK4tE20Otpl2jAdRzek+9OT6cX0H+i99AllJWVb5SjlHOUa5bPKUgbCMGD4M1IZpYyTjLuMj/M05rnP48/bNq9pXv+8KZX5Km4qfJUilWaVAZWPqkxVb9UU1Z2qbapP1DBqJmphatlq+9Uuq43Pp893ns+dXzT/5PyH6rC6iXq4+mr1w+o96pMamhq+GhkaVRqXNMY1GZpumsma5ZrnNMe0aFoLtQRa5VrntV4wlZnuzFRmJbOLOaGtru2nLdE+pN2rPa1jqLNYZ6NOs84TXZIuWzdBt1y3U3dCT0svWC9fr1HvoT5Rn62fpL9Hv1t/ysDQINpgi0GbwaihiqG/YZ5ho+FjI6qRq9Eqo1qjO8Y4Y7ZxivE+41smsImdSZJJjclNU9jU3lRgus+0zwxr5mgmNKs1u8eisNxZWaxG1qA5wzzIfKN5m/krCz2LWIudFt0WXyztLFMt6ywfWSlZBVhttOqw+sPaxJprXWN9x4Zq42Ozzqbd5rWtqS3fdr/tfTuaXbDdFrtOu8/2DvYi+yb7MQc9h3iHvQ732HR2KLuEfdUR6+jhuM7xjOMHJ3snsdNJp9+dWc4pzg3OowsMF/AX1C0YctFx4bgccpEuZC6MX3hwodRV25XjWuv6zE3Xjed2xG3E3dg92f24+ysPSw+RR4vHlKeT5xrPC16Il69XkVevt5L3Yu9q76c+Oj6JPo0+E752vqt9L/hh/QL9dvrd89fw5/rX+08EOASsCegKpARGBFYHPgsyCRIFdQTDwQHBu4IfL9JfJFzUFgJC/EN2hTwJNQxdFfpzGC4sNKwm7Hm4VXh+eHcELWJFREPEu0iPyNLIR4uNFksWd0bJR8VF1UdNRXtFl0VLl1gsWbPkRoxajCCmPRYfGxV7JHZyqffS3UuH4+ziCuPuLjNclrPs2nK15anLz66QX8FZcSoeGx8d3xD/iRPCqeVMrvRfuXflBNeTu4f7kufGK+eN8V34ZfyRBJeEsoTRRJfEXYljSa5JFUnjAk9BteB1sl/ygeSplJCUoykzqdGpzWmEtPi000IlYYqwK10zPSe9L8M0ozBDuspp1e5VE6JA0ZFMKHNZZruYjv5M9UiMJJslg1kLs2qy3mdHZZ/KUcwR5vTkmuRuyx3J88n7fjVmNXd1Z752/ob8wTXuaw6thdauXNu5Tnddwbrh9b7rj20gbUjZ8MtGy41lG99uit7UUaBRsL5gaLPv5sZCuUJR4b0tzlsObMVsFWzt3WazrWrblyJe0fViy+KK4k8l3JLr31l9V/ndzPaE7b2l9qX7d+B2CHfc3em681iZYlle2dCu4F2t5czyovK3u1fsvlZhW3FgD2mPZI+0MqiyvUqvakfVp+qk6oEaj5rmvep7t+2d2sfb17/fbX/TAY0DxQc+HhQcvH/I91BrrUFtxWHc4azDz+ui6rq/Z39ff0TtSPGRz0eFR6XHwo911TvU1zeoN5Q2wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HOFZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3RB6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/syOll+AAAACXBIWXMAAAsTAAALEwEAmpwYAAADqmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiCiAgICAgICAgICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyI+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnhtcC5kaWQ6M0ExNEY4NjZBNkU1MTFFMTlGMkFGQ0QyNTUyN0VDRjY8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpEZXJpdmVkRnJvbSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgIDxzdFJlZjppbnN0YW5jZUlEPnhtcC5paWQ6M0ExNEY4NjNBNkU1MTFFMTlGMkFGQ0QyNTUyN0VDRjY8L3N0UmVmOmluc3RhbmNlSUQ+CiAgICAgICAgICAgIDxzdFJlZjpkb2N1bWVudElEPnhtcC5kaWQ6M0ExNEY4NjRBNkU1MTFFMTlGMkFGQ0QyNTUyN0VDRjY8L3N0UmVmOmRvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6M0ExNEY4NjVBNkU1MTFFMTlGMkFGQ0QyNTUyN0VDRjY8L3htcE1NOkluc3RhbmNlSUQ+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2g8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+ChetDDYAAACaSURBVEgNY2AYboAR3UNnzpx5DxQTQBP/YGJiIogmBuYSq54Ji2Z0S0BKsInBtGKTwxDDZhHMAKrSdLOIqq7GZxjj+fPnBf7+/bseqMgBn0IK5A4wMzMHMtHYEpD7HEB2gOLIAcSjMXCgW2IYtYjsqBwNutGgg4fAaGKABwWpDLoG3QFSXUeG+gNMoEoJqJGWloErPjIcR54WALqPHeiJgl15AAAAAElFTkSuQmCC"; +const makeHTML = fn => + "data:text/html;charset=utf-8,"; + + +const test = function(unit) { + return function*(assert) { + assert.isRendered = (panel, toolbox) => { + const doc = toolbox.doc; + assert.ok(doc.querySelector("[value='" + panel.label + "']"), + "panel.label is found in the developer toolbox DOM"); + assert.ok(doc.querySelector("[tooltiptext='" + panel.tooltip + "']"), + "panel.tooltip is found in the developer toolbox DOM"); + + assert.ok(doc.querySelector("#toolbox-panel-" + panel.id), + "toolbar panel with a matching id is present"); + }; + + + yield* unit(assert); + }; +}; + +exports["test Panel API"] = test(function*(assert) { + const MyPanel = Class({ + extends: Panel, + label: "test panel", + tooltip: "my test panel", + icon: iconURI, + url: makeHTML(() => { + document.documentElement.innerHTML = "hello world"; + }), + setup: function({debuggee}) { + this.debuggee = debuggee; + assert.equal(this.readyState, "uninitialized", + "at construction time panel document is not inited"); + }, + dispose: function() { + delete this.debuggee; + } + }); + assert.ok(MyPanel, "panel is defined"); + + const myTool = new Tool({ + panels: { + myPanel: MyPanel + } + }); + assert.ok(myTool, "tool is defined"); + + + var toolbox = yield openToolbox(MyPanel); + var panel = yield getCurrentPanel(toolbox); + assert.ok(panel instanceof MyPanel, "is instance of MyPanel"); + + assert.isRendered(panel, toolbox); + + if (panel.readyState === "uninitialized") { + yield panel.ready(); + assert.equal(panel.readyState, "interactive", "panel is ready"); + } + + yield panel.loaded(); + assert.equal(panel.readyState, "complete", "panel is loaded"); + + yield closeToolbox(); + + assert.equal(panel.readyState, "destroyed", "panel is destroyed"); + + myTool.destroy(); +}); + +exports["test forbid remote https docs"] = test(function*(assert) { + const MyPanel = Class({ + extends: Panel, + label: "test https panel", + tooltip: "my test panel", + icon: iconURI, + url: "https://mozilla.org", + }); + + assert.throws(() => { + new Tool({ panels: { myPanel: MyPanel } }); + }, + /The `options.url` must be a valid local URI/, + "can't use panel with remote URI"); +}); + +exports["test forbid remote http docs"] = test(function*(assert) { + const MyPanel = Class({ + extends: Panel, + label: "test http panel", + tooltip: "my test panel", + icon: iconURI, + url: "http://arewefastyet.com/", + }); + + assert.throws(() => { + new Tool({ panels: { myPanel: MyPanel } }); + }, + /The `options.url` must be a valid local URI/, + "can't use panel with remote URI"); +}); + +exports["test forbid remote ftp docs"] = test(function*(assert) { + const MyPanel = Class({ + extends: Panel, + label: "test ftp panel", + tooltip: "my test panel", + icon: iconURI, + url: "ftp://ftp.mozilla.org/", + }); + + assert.throws(() => { + new Tool({ panels: { myPanel: MyPanel } }); + }, + /The `options.url` must be a valid local URI/, + "can't use panel with remote URI"); +}); + + +exports["test Panel communication"] = test(function*(assert) { + const MyPanel = Class({ + extends: Panel, + label: "communication", + tooltip: "test palen communication", + icon: iconURI, + url: makeHTML(() => { + window.addEventListener("message", event => { + if (event.source === window) { + var port = event.ports[0]; + port.start(); + port.postMessage("ping"); + port.onmessage = (event) => { + if (event.data === "pong") { + port.postMessage("bye"); + port.close(); + } + }; + } + }); + }), + dispose: function() { + delete this.port; + } + }); + + + const myTool = new Tool({ + panels: { + myPanel: MyPanel + } + }); + + + const toolbox = yield openToolbox(MyPanel); + const panel = yield getCurrentPanel(toolbox); + assert.ok(panel instanceof MyPanel, "is instance of MyPanel"); + + assert.isRendered(panel, toolbox); + + yield panel.ready(); + const { port1, port2 } = new MessageChannel(); + panel.port = port1; + panel.postMessage("connect", [port2]); + panel.port.start(); + + const ping = yield when(panel.port, "message"); + + assert.equal(ping.data, "ping", "received ping from panel doc"); + + panel.port.postMessage("pong"); + + const bye = yield when(panel.port, "message"); + + assert.equal(bye.data, "bye", "received bye from panel doc"); + + panel.port.close(); + + yield closeToolbox(); + + assert.equal(panel.readyState, "destroyed", "panel is destroyed"); + myTool.destroy(); +}); + +exports["test communication with debuggee"] = test(function*(assert) { + const MyPanel = Class({ + extends: Panel, + label: "debuggee", + tooltip: "test debuggee", + icon: iconURI, + url: makeHTML(() => { + window.addEventListener("message", event => { + if (event.source === window) { + var debuggee = event.ports[0]; + var port = event.ports[1]; + debuggee.start(); + port.start(); + + + debuggee.onmessage = (event) => { + port.postMessage(event.data); + }; + port.onmessage = (event) => { + debuggee.postMessage(event.data); + }; + } + }); + }), + setup: function({debuggee}) { + this.debuggee = debuggee; + }, + onReady: function() { + const { port1, port2 } = new MessageChannel(); + this.port = port1; + this.port.start(); + this.debuggee.start(); + + this.postMessage("connect", [this.debuggee, port2]); + }, + dispose: function() { + this.port.close(); + this.debuggee.close(); + + delete this.port; + delete this.debuggee; + } + }); + + + const myTool = new Tool({ + panels: { + myPanel: MyPanel + } + }); + + + const toolbox = yield openToolbox(MyPanel); + const panel = yield getCurrentPanel(toolbox); + assert.ok(panel instanceof MyPanel, "is instance of MyPanel"); + + assert.isRendered(panel, toolbox); + + yield panel.ready(); + const intro = yield when(panel.port, "message"); + + assert.equal(intro.data.from, "root", "intro message from root"); + + panel.port.postMessage({ + to: "root", + type: "echo", + text: "ping" + }); + + const pong = yield when(panel.port, "message"); + + assert.deepEqual(pong.data, { + to: "root", + from: "root", + type: "echo", + text: "ping" + }, "received message back from root"); + + yield closeToolbox(); + + assert.equal(panel.readyState, "destroyed", "panel is destroyed"); + + myTool.destroy(); +}); + + +exports["test viewFor panel"] = test(function*(assert) { + const url = "data:text/html;charset=utf-8,viewFor"; + const MyPanel = Class({ + extends: Panel, + label: "view for panel", + tooltip: "my panel view", + icon: iconURI, + url: url + }); + + const myTool = new Tool({ + panels: { + myPanel: MyPanel + } + }); + + + const toolbox = yield openToolbox(MyPanel); + const panel = yield getCurrentPanel(toolbox); + assert.ok(panel instanceof MyPanel, "is instance of MyPanel"); + + const frame = viewFor(panel); + + assert.equal(frame.nodeName.toLowerCase(), "iframe", + "viewFor(panel) returns associated iframe"); + + yield panel.loaded(); + + assert.equal(frame.contentDocument.URL, url, "is expected iframe"); + + yield closeToolbox(); + + myTool.destroy(); +}); + + +exports["test createView panel"] = test(function*(assert) { + var frame = null; + var panel = null; + + const url = "data:text/html;charset=utf-8,createView"; + const id = Math.random().toString(16).substr(2); + const MyPanel = Class({ + extends: Panel, + label: "create view", + tooltip: "panel creator", + icon: iconURI, + url: url + }); + + createView.define(MyPanel, (instance, document) => { + var view = document.createElement("iframe"); + view.setAttribute("type", "content"); + + // save instances for later asserts + frame = view; + panel = instance; + + return view; + }); + + const myTool = new Tool({ + panels: { + myPanel: MyPanel + } + }); + + const toolbox = yield openToolbox(MyPanel); + const myPanel = yield getCurrentPanel(toolbox); + + assert.equal(myPanel, panel, + "panel passed to createView is one instantiated"); + assert.equal(viewFor(panel), frame, + "createView has created an iframe"); + + yield panel.loaded(); + + assert.equal(frame.contentDocument.URL, url, "is expected iframe"); + + yield closeToolbox(); + + myTool.destroy(); +}); + + +exports["test ports is an optional"] = test(function*(assert) { + const MyPanel = Class({ + extends: Panel, + label: "no-port", + icon: iconURI, + url: makeHTML(() => { + window.addEventListener("message", event => { + if (event.ports.length) { + event.ports[0].postMessage(window.firstPacket); + } else { + window.firstPacket = event.data; + } + }); + }) + }); + + + const myTool = new Tool({ + panels: { + myPanel: MyPanel + } + }); + + + const toolbox = yield openToolbox(MyPanel); + const panel = yield getCurrentPanel(toolbox); + assert.ok(panel instanceof MyPanel, "is instance of MyPanel"); + + assert.isRendered(panel, toolbox); + + yield panel.ready(); + + const { port1, port2 } = new MessageChannel(); + port1.start(); + + panel.postMessage("hi"); + panel.postMessage("bye", [port2]); + + const packet = yield when(port1, "message"); + + assert.equal(packet.data, "hi", "got first packet back"); + + yield closeToolbox(); + + assert.equal(panel.readyState, "destroyed", "panel is destroyed"); + + myTool.destroy(); +}); + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-diffpatcher.js b/addon-sdk/source/test/test-diffpatcher.js new file mode 100644 index 000000000..47126c930 --- /dev/null +++ b/addon-sdk/source/test/test-diffpatcher.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"; + +exports["test diffpatcher"] = require("diffpatcher/test/index"); +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-dispatcher.js b/addon-sdk/source/test/test-dispatcher.js new file mode 100644 index 000000000..437d75176 --- /dev/null +++ b/addon-sdk/source/test/test-dispatcher.js @@ -0,0 +1,76 @@ +/* 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 { dispatcher } = require("sdk/util/dispatcher"); + +exports["test dispatcher API"] = assert => { + const dispatch = dispatcher(); + + assert.equal(typeof(dispatch), "function", + "dispatch is a function"); + + assert.equal(typeof(dispatch.define), "function", + "dispatch.define is a function"); + + assert.equal(typeof(dispatch.implement), "function", + "dispatch.implement is a function"); + + assert.equal(typeof(dispatch.when), "function", + "dispatch.when is a function"); +}; + +exports["test dispatcher"] = assert => { + const isDuck = dispatcher(); + + const quacks = x => x && typeof(x.quack) === "function"; + + const Duck = function() {}; + const Goose = function() {}; + + const True = _ => true; + const False = _ => false; + + + + isDuck.define(Goose, False); + isDuck.define(Duck, True); + isDuck.when(quacks, True); + + assert.equal(isDuck(new Goose()), false, + "Goose ain't duck"); + + assert.equal(isDuck(new Duck()), true, + "Ducks are ducks"); + + assert.equal(isDuck({ quack: () => "Quaaaaaack!" }), true, + "It's a duck if it quacks"); + + + assert.throws(() => isDuck({}), /Type does not implements method/, "not implemneted"); + + isDuck.define(Object, False); + + assert.equal(isDuck({}), false, + "Ain't duck if it does not quacks!"); +}; + +exports["test redefining fails"] = assert => { + const isPM = dispatcher(); + const isAfternoon = time => time.getHours() > 12; + + isPM.when(isAfternoon, _ => true); + + assert.equal(isPM(new Date(Date.parse("Jan 23, 1985, 13:20:00"))), true, + "yeap afternoon"); + assert.equal(isPM({ getHours: _ => 17 }), true, + "seems like afternoon"); + + assert.throws(() => isPM.when(isAfternoon, x => x > 12 && x < 24), + /Already implemented for the given predicate/, + "can't redefine on same predicate"); + +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-disposable.js b/addon-sdk/source/test/test-disposable.js new file mode 100644 index 000000000..3204c2479 --- /dev/null +++ b/addon-sdk/source/test/test-disposable.js @@ -0,0 +1,393 @@ +/* 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("sdk/test/loader"); +const { Class } = require("sdk/core/heritage"); +const { Disposable } = require("sdk/core/disposable"); +const { Cc, Ci, Cu } = require("chrome"); +const { setTimeout } = require("sdk/timers"); + +exports["test disposeDisposable"] = assert => { + let loader = Loader(module); + + const { Disposable, disposeDisposable } = loader.require("sdk/core/disposable"); + const { isWeak, WeakReference } = loader.require("sdk/core/reference"); + + let disposals = 0; + + const Foo = Class({ + extends: Disposable, + implements: [WeakReference], + dispose(...params) { + disposeDisposable(this); + disposals = disposals + 1; + } + }); + + const f1 = new Foo(); + assert.equal(isWeak(f1), true, "f1 has WeakReference support"); + + f1.dispose(); + assert.equal(disposals, 1, "disposed on dispose"); + + loader.unload("uninstall"); + assert.equal(disposals, 1, "after disposeDisposable, dispose is not called anymore"); +}; + +exports["test destroy reasons"] = assert => { + let disposals = 0; + + const Foo = Class({ + extends: Disposable, + dispose: function() { + disposals = disposals + 1; + } + }); + + const f1 = new Foo(); + + f1.destroy(); + assert.equal(disposals, 1, "disposed on destroy"); + f1.destroy(); + assert.equal(disposals, 1, "second destroy is ignored"); + + disposals = 0; + const f2 = new Foo(); + + f2.destroy("uninstall"); + assert.equal(disposals, 1, "uninstall invokes disposal"); + f2.destroy("uninstall") + f2.destroy(); + assert.equal(disposals, 1, "disposal happens just once"); + + disposals = 0; + const f3 = new Foo(); + + f3.destroy("shutdown"); + assert.equal(disposals, 0, "shutdown invoke disposal"); + f3.destroy("shutdown"); + f3.destroy(); + assert.equal(disposals, 0, "shutdown disposal happens just once"); + + disposals = 0; + const f4 = new Foo(); + + f4.destroy("disable"); + assert.equal(disposals, 1, "disable invokes disposal"); + f4.destroy("disable") + f4.destroy(); + assert.equal(disposals, 1, "destroy happens just once"); + + disposals = 0; + const f5 = new Foo(); + + f5.destroy("disable"); + assert.equal(disposals, 1, "disable invokes disposal"); + f5.destroy("disable") + f5.destroy(); + assert.equal(disposals, 1, "destroy happens just once"); + + disposals = 0; + const f6 = new Foo(); + + f6.destroy("upgrade"); + assert.equal(disposals, 1, "upgrade invokes disposal"); + f6.destroy("upgrade") + f6.destroy(); + assert.equal(disposals, 1, "destroy happens just once"); + + disposals = 0; + const f7 = new Foo(); + + f7.destroy("downgrade"); + assert.equal(disposals, 1, "downgrade invokes disposal"); + f7.destroy("downgrade") + f7.destroy(); + assert.equal(disposals, 1, "destroy happens just once"); + + + disposals = 0; + const f8 = new Foo(); + + f8.destroy("whatever"); + assert.equal(disposals, 1, "unrecognized reason invokes disposal"); + f8.destroy("meh") + f8.destroy(); + assert.equal(disposals, 1, "destroy happens just once"); +}; + +exports["test different unload hooks"] = assert => { + const { uninstall, shutdown, disable, upgrade, + downgrade, dispose } = require("sdk/core/disposable"); + const UberUnload = Class({ + extends: Disposable, + setup: function() { + this.log = []; + } + }); + + uninstall.define(UberUnload, x => x.log.push("uninstall")); + shutdown.define(UberUnload, x => x.log.push("shutdown")); + disable.define(UberUnload, x => x.log.push("disable")); + upgrade.define(UberUnload, x => x.log.push("upgrade")); + downgrade.define(UberUnload, x => x.log.push("downgrade")); + dispose.define(UberUnload, x => x.log.push("dispose")); + + const u1 = new UberUnload(); + u1.destroy("uninstall"); + u1.destroy(); + u1.destroy("shutdown"); + assert.deepEqual(u1.log, ["uninstall"], "uninstall hook invoked"); + + const u2 = new UberUnload(); + u2.destroy("shutdown"); + u2.destroy(); + u2.destroy("uninstall"); + assert.deepEqual(u2.log, ["shutdown"], "shutdown hook invoked"); + + const u3 = new UberUnload(); + u3.destroy("disable"); + u3.destroy(); + u3.destroy("uninstall"); + assert.deepEqual(u3.log, ["disable"], "disable hook invoked"); + + const u4 = new UberUnload(); + u4.destroy("upgrade"); + u4.destroy(); + u4.destroy("uninstall"); + assert.deepEqual(u4.log, ["upgrade"], "upgrade hook invoked"); + + const u5 = new UberUnload(); + u5.destroy("downgrade"); + u5.destroy(); + u5.destroy("uninstall"); + assert.deepEqual(u5.log, ["downgrade"], "downgrade hook invoked"); + + const u6 = new UberUnload(); + u6.destroy(); + u6.destroy(); + u6.destroy("uninstall"); + assert.deepEqual(u6.log, ["dispose"], "dispose hook invoked"); + + const u7 = new UberUnload(); + u7.destroy("whatever"); + u7.destroy(); + u7.destroy("uninstall"); + assert.deepEqual(u7.log, ["dispose"], "dispose hook invoked"); +}; + +exports["test disposables are disposed on unload"] = function(assert) { + let loader = Loader(module); + let { Disposable } = loader.require("sdk/core/disposable"); + + let arg1 = {} + let arg2 = 2 + let disposals = 0 + + let Foo = Class({ + extends: Disposable, + setup: function setup(a, b) { + assert.equal(a, arg1, + "arguments passed to constructur is passed to setup"); + assert.equal(b, arg2, + "second argument is also passed to a setup"); + assert.ok(this instanceof Foo, + "this is an instance in the scope of the setup method"); + + this.fooed = true + }, + dispose: function dispose() { + assert.equal(this.fooed, true, "attribute was set") + this.fooed = false + disposals = disposals + 1 + } + }) + + let foo1 = Foo(arg1, arg2) + let foo2 = Foo(arg1, arg2) + + loader.unload(); + + assert.equal(disposals, 2, "both instances were disposed") +} + +exports["test destroyed windows dispose before unload"] = function(assert) { + let loader = Loader(module); + let { Disposable } = loader.require("sdk/core/disposable"); + + let arg1 = {} + let arg2 = 2 + let disposals = 0 + + let Foo = Class({ + extends: Disposable, + setup: function setup(a, b) { + assert.equal(a, arg1, + "arguments passed to constructur is passed to setup"); + assert.equal(b, arg2, + "second argument is also passed to a setup"); + assert.ok(this instanceof Foo, + "this is an instance in the scope of the setup method"); + + this.fooed = true + }, + dispose: function dispose() { + assert.equal(this.fooed, true, "attribute was set") + this.fooed = false + disposals = disposals + 1 + } + }) + + let foo1 = Foo(arg1, arg2) + let foo2 = Foo(arg1, arg2) + + foo1.destroy(); + assert.equal(disposals, 1, "destroy disposes instance") + + loader.unload(); + + assert.equal(disposals, 2, "unload disposes alive instances") +} + +exports["test disposables are GC-able"] = function(assert, done) { + let loader = Loader(module); + let { Disposable } = loader.require("sdk/core/disposable"); + let { WeakReference } = loader.require("sdk/core/reference"); + + let arg1 = {} + let arg2 = 2 + let disposals = 0 + + let Foo = Class({ + extends: Disposable, + implements: [WeakReference], + setup: function setup(a, b) { + assert.equal(a, arg1, + "arguments passed to constructur is passed to setup"); + assert.equal(b, arg2, + "second argument is also passed to a setup"); + assert.ok(this instanceof Foo, + "this is an instance in the scope of the setup method"); + + this.fooed = true + }, + dispose: function dispose() { + assert.equal(this.fooed, true, "attribute was set") + this.fooed = false + disposals = disposals + 1 + } + }); + + let foo1 = Foo(arg1, arg2) + let foo2 = Foo(arg1, arg2) + + foo1 = foo2 = null; + + Cu.schedulePreciseGC(function() { + loader.unload(); + assert.equal(disposals, 0, "GC removed dispose listeners"); + done(); + }); +} + + +exports["test loader unloads do not affect other loaders"] = function(assert) { + let loader1 = Loader(module); + let loader2 = Loader(module); + let { Disposable: Disposable1 } = loader1.require("sdk/core/disposable"); + let { Disposable: Disposable2 } = loader2.require("sdk/core/disposable"); + + let arg1 = {} + let arg2 = 2 + let disposals = 0 + + let Foo1 = Class({ + extends: Disposable1, + dispose: function dispose() { + disposals = disposals + 1; + } + }); + + let Foo2 = Class({ + extends: Disposable2, + dispose: function dispose() { + disposals = disposals + 1; + } + }); + + let foo1 = Foo1(arg1, arg2); + let foo2 = Foo2(arg1, arg2); + + assert.equal(disposals, 0, "no destroy calls"); + + loader1.unload(); + + assert.equal(disposals, 1, "1 destroy calls"); + + loader2.unload(); + + assert.equal(disposals, 2, "2 destroy calls"); +} + +exports["test disposables that throw"] = function(assert) { + let loader = Loader(module); + let { Disposable } = loader.require("sdk/core/disposable"); + + let disposals = 0 + + let Foo = Class({ + extends: Disposable, + setup: function setup(a, b) { + throw Error("Boom!") + }, + dispose: function dispose() { + disposals = disposals + 1 + } + }) + + assert.throws(function() { + let foo1 = Foo() + }, /Boom/, "disposable constructors may throw"); + + loader.unload(); + + assert.equal(disposals, 0, "no disposal if constructor threw"); +} + +exports["test multiple destroy"] = function(assert) { + let loader = Loader(module); + let { Disposable } = loader.require("sdk/core/disposable"); + + let disposals = 0 + + let Foo = Class({ + extends: Disposable, + dispose: function dispose() { + disposals = disposals + 1 + } + }) + + let foo1 = Foo(); + let foo2 = Foo(); + let foo3 = Foo(); + + assert.equal(disposals, 0, "no disposals yet"); + + foo1.destroy(); + assert.equal(disposals, 1, "disposed properly"); + foo1.destroy(); + assert.equal(disposals, 1, "didn't attempt to dispose twice"); + + foo2.destroy(); + assert.equal(disposals, 2, "other instances still dispose fine"); + foo2.destroy(); + assert.equal(disposals, 2, "but not twice"); + + loader.unload(); + + assert.equal(disposals, 3, "unload only disposed the remaining instance"); +} + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-dom.js b/addon-sdk/source/test/test-dom.js new file mode 100644 index 000000000..0b50a7639 --- /dev/null +++ b/addon-sdk/source/test/test-dom.js @@ -0,0 +1,88 @@ +/* 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 events = require("sdk/dom/events"); +const { activeBrowserWindow: { document } } = require("sdk/deprecated/window-utils"); +const window = document.window; +/* +exports["test on / emit"] = function (assert, done) { + let element = document.createElement("div"); + events.on(element, "click", function listener(event) { + assert.equal(event.target, element, "event has correct target"); + events.removeListener(element, "click", listener); + done(); + }); + + events.emit(element, "click", { + category: "MouseEvents", + settings: [ + true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null + ] + }); +}; + +exports["test remove"] = function (assert, done) { + let element = document.createElement("span"); + let l1 = 0; + let l2 = 0; + let options = { + category: "MouseEvents", + settings: [ + true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null + ] + }; + + events.on(element, "click", function listener1(event) { + l1 ++; + assert.equal(event.target, element, "event has correct target"); + events.removeListener(element, "click", listener1); + }); + + events.on(element, "click", function listener2(event) { + l2 ++; + if (l1 < l2) { + assert.equal(l1, 1, "firs listener was called and then romeved"); + events.removeListener(element, "click", listener2); + done(); + } + events.emit(element, "click", options); + }); + + events.emit(element, "click", options); +}; + +exports["test once"] = function (assert, done) { + let element = document.createElement("h1"); + let l1 = 0; + let l2 = 0; + let options = { + category: "MouseEvents", + settings: [ + true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null + ] + }; + + + events.once(element, "click", function listener(event) { + assert.equal(event.target, element, "event target is a correct element"); + l1 ++; + }); + + events.on(element, "click", function listener(event) { + l2 ++; + if (l2 > 3) { + events.removeListener(element, "click", listener); + assert.equal(event.target, element, "event has correct target"); + assert.equal(l1, 1, "once was called only once"); + done(); + } + events.emit(element, "click", options); + }); + + events.emit(element, "click", options); +} +*/ +require("test").run(exports); diff --git a/addon-sdk/source/test/test-environment.js b/addon-sdk/source/test/test-environment.js new file mode 100644 index 000000000..9fec6f83d --- /dev/null +++ b/addon-sdk/source/test/test-environment.js @@ -0,0 +1,49 @@ +/* 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'); +const { Cc, Ci } = require('chrome'); +const { get, set, exists } = Cc['@mozilla.org/process/environment;1']. + getService(Ci.nsIEnvironment); + +exports['test exists'] = function(assert) { + assert.equal('PATH' in env, exists('PATH'), + 'PATH environment variable is defined'); + assert.equal('FOO1' in env, exists('FOO1'), + 'FOO1 environment variable is not defined'); + set('FOO1', 'foo'); + assert.equal('FOO1' in env, true, + 'FOO1 environment variable was set'); + set('FOO1', null); + assert.equal('FOO1' in env, false, + 'FOO1 environment variable was unset'); +}; + +exports['test get'] = function(assert) { + assert.equal(env.PATH, get('PATH'), 'PATH env variable matches'); + assert.equal(env.BAR2, undefined, 'BAR2 env variable is not defined'); + set('BAR2', 'bar'); + assert.equal(env.BAR2, 'bar', 'BAR2 env variable was set'); + set('BAR2', null); + assert.equal(env.BAR2, undefined, 'BAR2 env variable was unset'); +}; + +exports['test set'] = function(assert) { + assert.equal(get('BAZ3'), '', 'BAZ3 env variable is not set'); + assert.equal(env.BAZ3, undefined, 'BAZ3 is not set'); + env.BAZ3 = 'baz'; + assert.equal(env.BAZ3, get('BAZ3'), 'BAZ3 env variable is set'); + assert.equal(get('BAZ3'), 'baz', 'BAZ3 env variable was set to "baz"'); +}; + +exports['test unset'] = function(assert) { + env.BLA4 = 'bla'; + assert.equal(env.BLA4, 'bla', 'BLA4 env variable is set'); + assert.equal(delete env.BLA4, true, 'BLA4 env variable is removed'); + assert.equal(env.BLA4, undefined, 'BLA4 env variable is unset'); + assert.equal('BLA4' in env, false, 'BLA4 env variable no longer exists' ); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-event-core.js b/addon-sdk/source/test/test-event-core.js new file mode 100644 index 000000000..ca937b259 --- /dev/null +++ b/addon-sdk/source/test/test-event-core.js @@ -0,0 +1,347 @@ +/* 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, once, off, emit, count } = require('sdk/event/core'); +const { LoaderWithHookedConsole } = require("sdk/test/loader"); +const { defer } = require("sdk/core/promise"); +const { gc } = require("sdk/test/memory"); + +exports['test add a listener'] = function(assert) { + let events = [ { name: 'event#1' }, 'event#2' ]; + let target = { name: 'target' }; + + on(target, 'message', function(message) { + assert.equal(this, target, 'this is a target object'); + assert.equal(message, events.shift(), 'message is emitted event'); + }); + emit(target, 'message', events[0]); + emit(target, 'message', events[0]); +}; + +exports['test that listener is unique per type'] = function(assert) { + let actual = [] + let target = {} + function listener() { actual.push(1) } + on(target, 'message', listener); + on(target, 'message', listener); + on(target, 'message', listener); + on(target, 'foo', listener); + on(target, 'foo', listener); + + emit(target, 'message'); + assert.deepEqual([ 1 ], actual, 'only one message listener added'); + emit(target, 'foo'); + assert.deepEqual([ 1, 1 ], actual, 'same listener added for other event'); +}; + +exports['test event type matters'] = function(assert) { + let target = { name: 'target' } + on(target, 'message', function() { + assert.fail('no event is expected'); + }); + on(target, 'done', function() { + assert.pass('event is emitted'); + }); + emit(target, 'foo') + emit(target, 'done'); +}; + +exports['test all arguments are pasesd'] = function(assert) { + let foo = { name: 'foo' }, bar = 'bar'; + let target = { name: 'target' }; + on(target, 'message', function(a, b) { + assert.equal(a, foo, 'first argument passed'); + assert.equal(b, bar, 'second argument passed'); + }); + emit(target, 'message', foo, bar); +}; + +exports['test no side-effects in emit'] = function(assert) { + let target = { name: 'target' }; + on(target, 'message', function() { + assert.pass('first listener is called'); + on(target, 'message', function() { + assert.fail('second listener is called'); + }); + }); + emit(target, 'message'); +}; + +exports['test can remove next listener'] = function(assert) { + let target = { name: 'target' }; + function fail() { + return assert.fail('Listener should be removed'); + }; + + on(target, 'data', function() { + assert.pass('first litener called'); + off(target, 'data', fail); + }); + on(target, 'data', fail); + + emit(target, 'data', 'hello'); +}; + +exports['test order of propagation'] = function(assert) { + let actual = []; + let target = { name: 'target' }; + on(target, 'message', function() { actual.push(1); }); + on(target, 'message', function() { actual.push(2); }); + on(target, 'message', function() { actual.push(3); }); + emit(target, 'message'); + assert.deepEqual([ 1, 2, 3 ], actual, 'called in order they were added'); +}; + +exports['test remove a listener'] = function(assert) { + let target = { name: 'target' }; + let actual = []; + on(target, 'message', function listener() { + actual.push(1); + on(target, 'message', function() { + off(target, 'message', listener); + actual.push(2); + }) + }); + + emit(target, 'message'); + assert.deepEqual([ 1 ], actual, 'first listener called'); + emit(target, 'message'); + assert.deepEqual([ 1, 1, 2 ], actual, 'second listener called'); + + emit(target, 'message'); + assert.deepEqual([ 1, 1, 2, 2, 2 ], actual, 'first listener removed'); +}; + +exports['test remove all listeners for type'] = function(assert) { + let actual = []; + let target = { name: 'target' } + on(target, 'message', function() { actual.push(1); }); + on(target, 'message', function() { actual.push(2); }); + on(target, 'message', function() { actual.push(3); }); + on(target, 'bar', function() { actual.push('b') }); + off(target, 'message'); + + emit(target, 'message'); + emit(target, 'bar'); + + assert.deepEqual([ 'b' ], actual, 'all message listeners were removed'); +}; + +exports['test remove all listeners'] = function(assert) { + let actual = []; + let target = { name: 'target' } + on(target, 'message', function() { actual.push(1); }); + on(target, 'message', function() { actual.push(2); }); + on(target, 'message', function() { actual.push(3); }); + on(target, 'bar', function() { actual.push('b') }); + off(target); + + emit(target, 'message'); + emit(target, 'bar'); + + assert.deepEqual([], actual, 'all listeners events were removed'); +}; + +exports['test falsy arguments are fine'] = function(assert) { + let type, listener, actual = []; + let target = { name: 'target' } + on(target, 'bar', function() { actual.push(0) }); + + off(target, 'bar', listener); + emit(target, 'bar'); + assert.deepEqual([ 0 ], actual, '3rd bad ard will keep listeners'); + + off(target, type); + emit(target, 'bar'); + assert.deepEqual([ 0, 0 ], actual, '2nd bad arg will keep listener'); + + off(target, type, listener); + emit(target, 'bar'); + assert.deepEqual([ 0, 0, 0 ], actual, '2nd&3rd bad args will keep listener'); +}; + +exports['test error handling'] = function(assert) { + let target = Object.create(null); + let error = Error('boom!'); + + on(target, 'message', function() { throw error; }) + on(target, 'error', function(boom) { + assert.equal(boom, error, 'thrown exception causes error event'); + }); + emit(target, 'message'); +}; + +exports['test unhandled exceptions'] = function(assert) { + let exceptions = []; + let { loader, messages } = LoaderWithHookedConsole(module); + + let { emit, on } = loader.require('sdk/event/core'); + let target = {}; + let boom = Error('Boom!'); + let drax = Error('Draax!!'); + + on(target, 'message', function() { throw boom; }); + + emit(target, 'message'); + assert.equal(messages.length, 1, 'Got the first exception'); + assert.equal(messages[0].type, 'exception', 'The console message is exception'); + assert.ok(~String(messages[0].msg).indexOf('Boom!'), + 'unhandled exception is logged'); + + on(target, 'error', function() { throw drax; }); + emit(target, 'message'); + assert.equal(messages.length, 2, 'Got the second exception'); + assert.equal(messages[1].type, 'exception', 'The console message is exception'); + assert.ok(~String(messages[1].msg).indexOf('Draax!'), + 'error in error handler is logged'); +}; + +exports['test unhandled errors'] = function(assert) { + let exceptions = []; + let { loader, messages } = LoaderWithHookedConsole(module); + + let { emit } = loader.require('sdk/event/core'); + let target = {}; + let boom = Error('Boom!'); + + emit(target, 'error', boom); + assert.equal(messages.length, 1, 'Error was logged'); + assert.equal(messages[0].type, 'exception', 'The console message is exception'); + assert.ok(~String(messages[0].msg).indexOf('Boom!'), + 'unhandled exception is logged'); +}; + +exports['test piped errors'] = function(assert) { + let exceptions = []; + let { loader, messages } = LoaderWithHookedConsole(module); + + let { emit } = loader.require('sdk/event/core'); + let { pipe } = loader.require('sdk/event/utils'); + let target = {}; + let second = {}; + + pipe(target, second); + emit(target, 'error', 'piped!'); + + assert.equal(messages.length, 1, 'error logged only once, ' + + 'considered "handled" on `target` by the catch-all pipe'); + assert.equal(messages[0].type, 'exception', 'The console message is exception'); + assert.ok(~String(messages[0].msg).indexOf('piped!'), + 'unhandled (piped) exception is logged on `second` target'); +}; + +exports['test count'] = function(assert) { + let target = {}; + + assert.equal(count(target, 'foo'), 0, 'no listeners for "foo" events'); + on(target, 'foo', function() {}); + assert.equal(count(target, 'foo'), 1, 'listener registered'); + on(target, 'foo', function() {}, 2, 'another listener registered'); + off(target) + assert.equal(count(target, 'foo'), 0, 'listeners unregistered'); +}; + +exports['test listen to all events'] = function(assert) { + let actual = []; + let target = {}; + + on(target, 'foo', message => actual.push(message)); + on(target, '*', (type, ...message) => { + actual.push([type].concat(message)); + }); + + emit(target, 'foo', 'hello'); + assert.equal(actual[0], 'hello', + 'non-wildcard listeners still work'); + assert.deepEqual(actual[1], ['foo', 'hello'], + 'wildcard listener called'); + + emit(target, 'bar', 'goodbye'); + assert.deepEqual(actual[2], ['bar', 'goodbye'], + 'wildcard listener called for unbound event name'); +}; + +exports['test once'] = function(assert, done) { + let target = {}; + let called = false; + let { resolve, promise } = defer(); + + once(target, 'foo', function(value) { + assert.ok(!called, "listener called only once"); + assert.equal(value, "bar", "correct argument was passed"); + }); + once(target, 'done', resolve); + + emit(target, 'foo', 'bar'); + emit(target, 'foo', 'baz'); + emit(target, 'done', ""); + + yield promise; +}; + +exports['test once with gc'] = function*(assert) { + let target = {}; + let called = false; + let { resolve, promise } = defer(); + + once(target, 'foo', function(value) { + assert.ok(!called, "listener called only once"); + assert.equal(value, "bar", "correct argument was passed"); + }); + once(target, 'done', resolve); + + yield gc(); + + emit(target, 'foo', 'bar'); + emit(target, 'foo', 'baz'); + emit(target, 'done', ""); + + yield promise; +}; + +exports["test removing once"] = function(assert, done) { + let target = {}; + + function test() { + assert.fail("listener was called"); + } + + once(target, "foo", test); + once(target, "done", done); + + off(target, "foo", test); + + assert.pass("emit foo"); + emit(target, "foo", "bar"); + + assert.pass("emit done"); + emit(target, "done", ""); +}; + +exports["test removing once with gc"] = function*(assert) { + let target = {}; + let { resolve, promise } = defer(); + + function test() { + assert.fail("listener was called"); + } + + once(target, "foo", test); + once(target, "done", resolve); + + yield gc(); + + off(target, "foo", test); + + assert.pass("emit foo"); + emit(target, "foo", "bar"); + + assert.pass("emit done"); + emit(target, "done", ""); + + yield promise; +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-event-dom.js b/addon-sdk/source/test/test-event-dom.js new file mode 100644 index 000000000..fbbc6825b --- /dev/null +++ b/addon-sdk/source/test/test-event-dom.js @@ -0,0 +1,92 @@ +/* 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 { openWindow, closeWindow } = require('./util'); +const { Loader } = require('sdk/test/loader'); +const { getMostRecentBrowserWindow } = require('sdk/window/utils'); +const { Cc, Ci } = require('chrome'); +const els = Cc["@mozilla.org/eventlistenerservice;1"]. + getService(Ci.nsIEventListenerService); + +function countListeners(target, type) { + let listeners = els.getListenerInfoFor(target, {}); + return listeners.filter(listener => listener.type == type).length; +} + +exports['test window close clears listeners'] = function(assert) { + let window = yield openWindow(); + let loader = Loader(module); + + // Any element will do here + let gBrowser = window.gBrowser; + + // Other parts of the app may be listening for this + let windowListeners = countListeners(window, "DOMWindowClose"); + + // We can assume we're the only ones using the test events + assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1"); + assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2"); + + let { open } = loader.require('sdk/event/dom'); + + open(gBrowser, "TestEvent1"); + assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1, + "Should have added a DOMWindowClose listener"); + assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1"); + assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2"); + + open(gBrowser, "TestEvent2"); + assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1, + "Should not have added another DOMWindowClose listener"); + assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1"); + assert.equal(countListeners(gBrowser, "TestEvent2"), 1, "Should be a listener for test event 2"); + + window = yield closeWindow(window); + + assert.equal(countListeners(window, "DOMWindowClose"), windowListeners, + "Should have removed a DOMWindowClose listener"); + assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1"); + assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2"); + + loader.unload(); +}; + +exports['test unload clears listeners'] = function(assert) { + let window = getMostRecentBrowserWindow(); + let loader = Loader(module); + + // Any element will do here + let gBrowser = window.gBrowser; + + // Other parts of the app may be listening for this + let windowListeners = countListeners(window, "DOMWindowClose"); + + // We can assume we're the only ones using the test events + assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1"); + assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2"); + + let { open } = loader.require('sdk/event/dom'); + + open(gBrowser, "TestEvent1"); + assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1, + "Should have added a DOMWindowClose listener"); + assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1"); + assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2"); + + open(gBrowser, "TestEvent2"); + assert.equal(countListeners(window, "DOMWindowClose"), windowListeners + 1, + "Should not have added another DOMWindowClose listener"); + assert.equal(countListeners(gBrowser, "TestEvent1"), 1, "Should be a listener for test event 1"); + assert.equal(countListeners(gBrowser, "TestEvent2"), 1, "Should be a listener for test event 2"); + + loader.unload(); + + assert.equal(countListeners(window, "DOMWindowClose"), windowListeners, + "Should have removed a DOMWindowClose listener"); + assert.equal(countListeners(gBrowser, "TestEvent1"), 0, "Should be no listener for test event 1"); + assert.equal(countListeners(gBrowser, "TestEvent2"), 0, "Should be no listener for test event 2"); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-event-target.js b/addon-sdk/source/test/test-event-target.js new file mode 100644 index 000000000..d51314aa5 --- /dev/null +++ b/addon-sdk/source/test/test-event-target.js @@ -0,0 +1,222 @@ +/* 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 { emit } = require('sdk/event/core'); +const { EventTarget } = require('sdk/event/target'); +const { Loader } = require('sdk/test/loader'); + +exports['test add a listener'] = function(assert) { + let events = [ { name: 'event#1' }, 'event#2' ]; + let target = EventTarget(); + + target.on('message', function(message) { + assert.equal(this, target, 'this is a target object'); + assert.equal(message, events.shift(), 'message is emitted event'); + }); + + emit(target, 'message', events[0]); + emit(target, 'message', events[0]); +}; + +exports['test pass in listeners'] = function(assert) { + let actual = [ ]; + let target = EventTarget({ + onMessage: function onMessage(message) { + assert.equal(this, target, 'this is an event target'); + actual.push(1); + }, + onFoo: null, + onbla: function() { + assert.fail('`onbla` is not supposed to be called'); + } + }); + target.on('message', function(message) { + assert.equal(this, target, 'this is an event target'); + actual.push(2); + }); + + emit(target, 'message'); + emit(target, 'missing'); + + assert.deepEqual([ 1, 2 ], actual, 'all listeners trigerred in right order'); +}; + +exports['test that listener is unique per type'] = function(assert) { + let actual = [] + let target = EventTarget(); + function listener() { actual.push(1) } + target.on('message', listener); + target.on('message', listener); + target.on('message', listener); + target.on('foo', listener); + target.on('foo', listener); + + emit(target, 'message'); + assert.deepEqual([ 1 ], actual, 'only one message listener added'); + emit(target, 'foo'); + assert.deepEqual([ 1, 1 ], actual, 'same listener added for other event'); +}; + +exports['test event type matters'] = function(assert) { + let target = EventTarget(); + target.on('message', function() { + assert.fail('no event is expected'); + }); + target.on('done', function() { + assert.pass('event is emitted'); + }); + + emit(target, 'foo'); + emit(target, 'done'); +}; + +exports['test all arguments are pasesd'] = function(assert) { + let foo = { name: 'foo' }, bar = 'bar'; + let target = EventTarget(); + target.on('message', function(a, b) { + assert.equal(a, foo, 'first argument passed'); + assert.equal(b, bar, 'second argument passed'); + }); + emit(target, 'message', foo, bar); +}; + +exports['test no side-effects in emit'] = function(assert) { + let target = EventTarget(); + target.on('message', function() { + assert.pass('first listener is called'); + target.on('message', function() { + assert.fail('second listener is called'); + }); + }); + emit(target, 'message'); +}; + +exports['test order of propagation'] = function(assert) { + let actual = []; + let target = EventTarget(); + target.on('message', function() { actual.push(1); }); + target.on('message', function() { actual.push(2); }); + target.on('message', function() { actual.push(3); }); + emit(target, 'message'); + assert.deepEqual([ 1, 2, 3 ], actual, 'called in order they were added'); +}; + +exports['test remove a listener'] = function(assert) { + let target = EventTarget(); + let actual = []; + target.on('message', function listener() { + actual.push(1); + target.on('message', function() { + target.removeListener('message', listener); + actual.push(2); + }) + }); + + emit(target, 'message'); + assert.deepEqual([ 1 ], actual, 'first listener called'); + emit(target, 'message'); + assert.deepEqual([ 1, 1, 2 ], actual, 'second listener called'); + emit(target, 'message'); + assert.deepEqual([ 1, 1, 2, 2, 2 ], actual, 'first listener removed'); +}; + +exports['test .off() removes all listeners'] = function(assert) { + let target = EventTarget(); + let actual = []; + target.on('message', function listener() { + actual.push(1); + target.on('message', function() { + target.removeListener('message', listener); + actual.push(2); + }) + }); + + emit(target, 'message'); + assert.deepEqual([ 1 ], actual, 'first listener called'); + emit(target, 'message'); + assert.deepEqual([ 1, 1, 2 ], actual, 'second listener called'); + target.off(); + emit(target, 'message'); + assert.deepEqual([ 1, 1, 2 ], actual, 'target.off() removed all listeners'); +}; + +exports['test error handling'] = function(assert) { + let target = EventTarget(); + let error = Error('boom!'); + + target.on('message', function() { throw error; }) + target.on('error', function(boom) { + assert.equal(boom, error, 'thrown exception causes error event'); + }); + emit(target, 'message'); +}; + +exports['test unhandled errors'] = function(assert) { + let exceptions = []; + let loader = Loader(module); + let { emit } = loader.require('sdk/event/core'); + let { EventTarget } = loader.require('sdk/event/target'); + Object.defineProperties(loader.sandbox('sdk/event/core'), { + console: { value: { + exception: function(e) { + exceptions.push(e); + } + }} + }); + let target = EventTarget(); + let boom = Error('Boom!'); + let drax = Error('Draax!!'); + + target.on('message', function() { throw boom; }); + + emit(target, 'message'); + assert.ok(~String(exceptions[0]).indexOf('Boom!'), + 'unhandled exception is logged'); + + target.on('error', function() { throw drax; }); + emit(target, 'message'); + assert.ok(~String(exceptions[1]).indexOf('Draax!'), + 'error in error handler is logged'); +}; + +exports['test target is chainable'] = function (assert, done) { + let loader = Loader(module); + let exceptions = []; + let { EventTarget } = loader.require('sdk/event/target'); + let { emit } = loader.require('sdk/event/core'); + Object.defineProperties(loader.sandbox('sdk/event/core'), { + console: { value: { + exception: function(e) { + exceptions.push(e); + } + }} + }); + + let emitter = EventTarget(); + let boom = Error('Boom'); + let onceCalled = 0; + + emitter.once('oneTime', function () { + assert.equal(++onceCalled, 1, 'once event called only once'); + }).on('data', function (message) { + assert.equal(message, 'message', 'handles event'); + emit(emitter, 'oneTime'); + emit(emitter, 'data2', 'message2'); + }).on('phony', function () { + assert.fail('removeListener does not remove chained event'); + }).removeListener('phony') + .on('data2', function (message) { + assert.equal(message, 'message2', 'handle chained event'); + emit(emitter, 'oneTime'); + throw boom; + }).on('error', function (error) { + assert.equal(error, boom, 'error handled in chained event'); + done(); + }); + + emit(emitter, 'data', 'message'); +}; + +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 new file mode 100644 index 000000000..ea69e7677 --- /dev/null +++ b/addon-sdk/source/test/test-event-utils.js @@ -0,0 +1,285 @@ +/* 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"); +const { filter, map, merge, expand, pipe, stripListeners } = require("sdk/event/utils"); +const $ = require("./event/helpers"); + +function isEven(x) { + return !(x % 2); +} +function inc(x) { + return x + 1; +} + +exports["test filter events"] = function(assert) { + let input = {}; + let evens = filter(input, isEven); + let actual = []; + on(evens, "data", e => actual.push(e)); + + [1, 2, 3, 4, 5, 6, 7].forEach(x => emit(input, "data", x)); + + assert.deepEqual(actual, [2, 4, 6], "only even numbers passed through"); +}; + +exports["test filter emits"] = $.emits(function(input, assert) { + let output = filter(input, isEven); + assert(output, [1, 2, 3, 4, 5], [2, 4], "this is `output` & evens passed"); +});; + +exports["test filter reg once"] = $.registerOnce(function(input, assert) { + assert(filter(input, isEven), [1, 2, 3, 4, 5, 6], [2, 4, 6], + "listener can be registered only once"); +}); + +exports["test filter ignores new"] = $.ignoreNew(function(input, assert) { + assert(filter(input, isEven), [1, 2, 3], [2], + "new listener is ignored") +}); + +exports["test filter is FIFO"] = $.FIFO(function(input, assert) { + assert(filter(input, isEven), [1, 2, 3, 4], [2, 4], + "listeners are invoked in fifo order") +}); + +exports["test map events"] = function(assert) { + let input = {}; + let incs = map(input, inc); + let actual = []; + on(incs, "data", e => actual.push(e)); + + [1, 2, 3, 4].forEach(x => emit(input, "data", x)); + + assert.deepEqual(actual, [2, 3, 4, 5], "all numbers were incremented"); +}; + +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], + "listener can be registered only once"); +}); + +exports["test map ignores new"] = $.ignoreNew(function(input, assert) { + assert(map(input, inc), [1], [2], + "new listener is ignored") +}); + +exports["test map is FIFO"] = $.FIFO(function(input, assert) { + assert(map(input, inc), [1, 2, 3, 4], [2, 3, 4, 5], + "listeners are invoked in fifo order") +}); + +exports["test merge stream[stream]"] = function(assert) { + let a = {}, b = {}, c = {}; + let inputs = {}; + let actual = []; + + on(merge(inputs), "data", $ => actual.push($)) + + emit(inputs, "data", a); + emit(a, "data", "a1"); + emit(inputs, "data", b); + emit(b, "data", "b1"); + emit(a, "data", "a2"); + emit(inputs, "data", c); + emit(c, "data", "c1"); + emit(c, "data", "c2"); + emit(b, "data", "b2"); + emit(a, "data", "a3"); + + assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"], + "all inputs data merged into one"); +}; + +exports["test merge array[stream]"] = function(assert) { + let a = {}, b = {}, c = {}; + let inputs = {}; + let actual = []; + + on(merge([a, b, c]), "data", $ => actual.push($)) + + emit(a, "data", "a1"); + emit(b, "data", "b1"); + emit(a, "data", "a2"); + emit(c, "data", "c1"); + emit(c, "data", "c2"); + emit(b, "data", "b2"); + emit(a, "data", "a3"); + + assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"], + "all inputs data merged into one"); +}; + +exports["test merge emits"] = $.emits(function(input, assert) { + let evens = filter(input, isEven) + let output = merge([evens, input]); + assert(output, [1, 2, 3], [1, 2, 2, 3], "this is `output` & evens passed"); +}); + + +exports["test merge reg once"] = $.registerOnce(function(input, assert) { + let evens = filter(input, isEven) + let output = merge([input, evens]); + assert(output, [1, 2, 3, 4], [1, 2, 2, 3, 4, 4], + "listener can be registered only once"); +}); + +exports["test merge ignores new"] = $.ignoreNew(function(input, assert) { + let evens = filter(input, isEven) + let output = merge([input, evens]) + assert(output, [1], [1], + "new listener is ignored") +}); + +exports["test marge is FIFO"] = $.FIFO(function(input, assert) { + let evens = filter(input, isEven) + let output = merge([input, evens]) + + assert(output, [1, 2, 3, 4], [1, 2, 2, 3, 4, 4], + "listeners are invoked in fifo order") +}); + +exports["test expand"] = function(assert) { + let a = {}, b = {}, c = {}; + let inputs = {}; + let actual = []; + + on(expand(inputs, $ => $()), "data", $ => actual.push($)) + + emit(inputs, "data", () => a); + emit(a, "data", "a1"); + emit(inputs, "data", () => b); + emit(b, "data", "b1"); + emit(a, "data", "a2"); + emit(inputs, "data", () => c); + emit(c, "data", "c1"); + emit(c, "data", "c2"); + emit(b, "data", "b2"); + emit(a, "data", "a3"); + + assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"], + "all inputs data merged into one"); +}; + +exports["test pipe"] = function (assert, done) { + let src = {}; + let dest = {}; + + let aneventCount = 0, multiargsCount = 0; + let wildcardCount = {}; + + on(dest, 'an-event', arg => { + assert.equal(arg, 'my-arg', 'piped argument to event'); + ++aneventCount; + check(); + }); + on(dest, 'multiargs', (...data) => { + assert.equal(data[0], 'a', 'multiple arguments passed via pipe'); + assert.equal(data[1], 'b', 'multiple arguments passed via pipe'); + assert.equal(data[2], 'c', 'multiple arguments passed via pipe'); + ++multiargsCount; + check(); + }); + + on(dest, '*', (name, ...data) => { + wildcardCount[name] = (wildcardCount[name] || 0) + 1; + if (name === 'multiargs') { + assert.equal(data[0], 'a', 'multiple arguments passed via pipe, wildcard'); + assert.equal(data[1], 'b', 'multiple arguments passed via pipe, wildcard'); + assert.equal(data[2], 'c', 'multiple arguments passed via pipe, wildcard'); + } + if (name === 'an-event') + assert.equal(data[0], 'my-arg', 'argument passed via pipe, wildcard'); + check(); + }); + + pipe(src, dest); + + for (let i = 0; i < 3; i++) + emit(src, 'an-event', 'my-arg'); + + emit(src, 'multiargs', 'a', 'b', 'c'); + + function check () { + if (aneventCount === 3 && multiargsCount === 1 && + wildcardCount['an-event'] === 3 && + wildcardCount['multiargs'] === 1) + done(); + } +}; + +exports["test pipe multiple targets"] = function (assert) { + let src1 = {}; + let src2 = {}; + let middle = {}; + let dest = {}; + + pipe(src1, middle); + pipe(src2, middle); + pipe(middle, dest); + + let middleFired = 0; + let destFired = 0; + let src1Fired = 0; + let src2Fired = 0; + + on(src1, '*', () => src1Fired++); + on(src2, '*', () => src2Fired++); + on(middle, '*', () => middleFired++); + on(dest, '*', () => destFired++); + + emit(src1, 'ev'); + assert.equal(src1Fired, 1, 'event triggers in source in pipe chain'); + assert.equal(middleFired, 1, 'event passes through the middle of pipe chain'); + assert.equal(destFired, 1, 'event propagates to end of pipe chain'); + assert.equal(src2Fired, 0, 'event does not fire on alternative chain routes'); + + emit(src2, 'ev'); + assert.equal(src2Fired, 1, 'event triggers in source in pipe chain'); + assert.equal(middleFired, 2, + 'event passes through the middle of pipe chain from different src'); + assert.equal(destFired, 2, + 'event propagates to end of pipe chain from different src'); + assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes'); + + emit(middle, 'ev'); + assert.equal(middleFired, 3, + 'event triggers in source of pipe chain'); + assert.equal(destFired, 3, + 'event propagates to end of pipe chain from middle src'); + assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes'); + assert.equal(src2Fired, 1, 'event does not fire on alternative chain routes'); +}; + +exports['test stripListeners'] = function (assert) { + var options = { + onAnEvent: noop1, + onMessage: noop2, + verb: noop1, + value: 100 + }; + + var stripped = stripListeners(options); + assert.ok(stripped !== options, 'stripListeners should return a new object'); + assert.equal(options.onAnEvent, noop1, 'stripListeners does not affect original'); + assert.equal(options.onMessage, noop2, 'stripListeners does not affect original'); + assert.equal(options.verb, noop1, 'stripListeners does not affect original'); + assert.equal(options.value, 100, 'stripListeners does not affect original'); + + assert.ok(!stripped.onAnEvent, 'stripListeners removes `on*` values'); + assert.ok(!stripped.onMessage, 'stripListeners removes `on*` values'); + assert.equal(stripped.verb, noop1, 'stripListeners leaves not `on*` values'); + assert.equal(stripped.value, 100, 'stripListeners leaves not `on*` values'); + + function noop1 () {} + function noop2 () {} +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-file.js b/addon-sdk/source/test/test-file.js new file mode 100644 index 000000000..268c7f791 --- /dev/null +++ b/addon-sdk/source/test/test-file.js @@ -0,0 +1,271 @@ +/* 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 { pathFor } = require('sdk/system'); +const file = require("sdk/io/file"); +const url = require("sdk/url"); + +const byteStreams = require("sdk/io/byte-streams"); +const textStreams = require("sdk/io/text-streams"); + +const ERRORS = { + FILE_NOT_FOUND: /^path does not exist: .+$/, + NOT_A_DIRECTORY: /^path is not a directory: .+$/, + NOT_A_FILE: /^path is not a file: .+$/, +}; + +// Use profile directory to list / read / write files. +const profilePath = pathFor('ProfD'); +const fileNameInProfile = 'compatibility.ini'; +const dirNameInProfile = 'extensions'; +const filePathInProfile = file.join(profilePath, fileNameInProfile); +const dirPathInProfile = file.join(profilePath, dirNameInProfile); + +exports.testDirName = function(assert) { + assert.equal(file.dirname(dirPathInProfile), profilePath, + "file.dirname() of dir should return parent dir"); + + assert.equal(file.dirname(filePathInProfile), profilePath, + "file.dirname() of file should return its dir"); + + let dir = profilePath; + while (dir) + dir = file.dirname(dir); + + assert.equal(dir, "", + "dirname should return empty string when dir has no parent"); +}; + +exports.testBasename = function(assert) { + // Get the top-most path -- the path with no basename. E.g., on Unix-like + // systems this will be /. We'll use it below to build up some test paths. + // We have to go to this trouble because file.join() needs a legal path as a + // base case; join("foo", "bar") doesn't work unfortunately. + let topPath = profilePath; + let parentPath = file.dirname(topPath); + while (parentPath) { + topPath = parentPath; + parentPath = file.dirname(topPath); + } + + let path = topPath; + assert.equal(file.basename(path), "", + "basename should work on paths with no components"); + + path = file.join(path, "foo"); + assert.equal(file.basename(path), "foo", + "basename should work on paths with a single component"); + + path = file.join(path, "bar"); + assert.equal(file.basename(path), "bar", + "basename should work on paths with multiple components"); +}; + +exports.testList = function(assert) { + let list = file.list(profilePath); + let found = list.filter(name => name === fileNameInProfile); + + assert.equal(found.length, 1, "file.list() should work"); + + assert.throws(function() { + file.list(filePathInProfile); + }, ERRORS.NOT_A_DIRECTORY, "file.list() on non-dir should raise error"); + + assert.throws(function() { + file.list(file.join(dirPathInProfile, "does-not-exist")); + }, ERRORS.FILE_NOT_FOUND, "file.list() on nonexistent should raise error"); +}; + +exports.testRead = function(assert) { + let contents = file.read(filePathInProfile); + assert.ok(/Compatibility/.test(contents), + "file.read() should work"); + + assert.throws(function() { + file.read(file.join(dirPathInProfile, "does-not-exists")); + }, ERRORS.FILE_NOT_FOUND, "file.read() on nonexistent file should throw"); + + assert.throws(function() { + file.read(dirPathInProfile); + }, ERRORS.NOT_A_FILE, "file.read() on dir should throw"); +}; + +exports.testJoin = function(assert) { + let baseDir = file.dirname(filePathInProfile); + + assert.equal(file.join(baseDir, fileNameInProfile), + filePathInProfile, "file.join() should work"); +}; + +exports.testOpenNonexistentForRead = function (assert) { + var filename = file.join(profilePath, 'does-not-exists'); + assert.throws(function() { + file.open(filename); + }, ERRORS.FILE_NOT_FOUND, "file.open() on nonexistent file should throw"); + + assert.throws(function() { + file.open(filename, "r"); + }, ERRORS.FILE_NOT_FOUND, "file.open('r') on nonexistent file should throw"); + + assert.throws(function() { + file.open(filename, "zz"); + }, ERRORS.FILE_NOT_FOUND, "file.open('zz') on nonexistent file should throw"); +}; + +exports.testOpenNonexistentForWrite = function (assert) { + let filename = file.join(profilePath, 'open.txt'); + + let stream = file.open(filename, "w"); + stream.close(); + + assert.ok(file.exists(filename), + "file.exists() should return true after file.open('w')"); + file.remove(filename); + assert.ok(!file.exists(filename), + "file.exists() should return false after file.remove()"); + + stream = file.open(filename, "rw"); + stream.close(); + + assert.ok(file.exists(filename), + "file.exists() should return true after file.open('rw')"); + file.remove(filename); + assert.ok(!file.exists(filename), + "file.exists() should return false after file.remove()"); +}; + +exports.testOpenDirectory = function (assert) { + let dir = dirPathInProfile; + assert.throws(function() { + file.open(dir); + }, ERRORS.NOT_A_FILE, "file.open() on directory should throw"); + + assert.throws(function() { + file.open(dir, "w"); + }, ERRORS.NOT_A_FILE, "file.open('w') on directory should throw"); +}; + +exports.testOpenTypes = function (assert) { + let filename = file.join(profilePath, 'open-types.txt'); + + + // Do the opens first to create the data file. + var stream = file.open(filename, "w"); + assert.ok(stream instanceof textStreams.TextWriter, + "open(w) should return a TextWriter"); + stream.close(); + + stream = file.open(filename, "wb"); + assert.ok(stream instanceof byteStreams.ByteWriter, + "open(wb) should return a ByteWriter"); + stream.close(); + + stream = file.open(filename); + assert.ok(stream instanceof textStreams.TextReader, + "open() should return a TextReader"); + stream.close(); + + stream = file.open(filename, "r"); + assert.ok(stream instanceof textStreams.TextReader, + "open(r) should return a TextReader"); + stream.close(); + + stream = file.open(filename, "b"); + assert.ok(stream instanceof byteStreams.ByteReader, + "open(b) should return a ByteReader"); + stream.close(); + + stream = file.open(filename, "rb"); + assert.ok(stream instanceof byteStreams.ByteReader, + "open(rb) should return a ByteReader"); + stream.close(); + + file.remove(filename); +}; + +exports.testMkpathRmdir = function (assert) { + let basePath = profilePath; + let dirs = []; + for (let i = 0; i < 3; i++) + dirs.push("test-file-dir"); + + let paths = []; + for (let i = 0; i < dirs.length; i++) { + let args = [basePath].concat(dirs.slice(0, i + 1)); + paths.unshift(file.join.apply(null, args)); + } + + for (let i = 0; i < paths.length; i++) { + assert.ok(!file.exists(paths[i]), + "Sanity check: path should not exist: " + paths[i]); + } + + file.mkpath(paths[0]); + assert.ok(file.exists(paths[0]), "mkpath should create path: " + paths[0]); + + for (let i = 0; i < paths.length; i++) { + file.rmdir(paths[i]); + assert.ok(!file.exists(paths[i]), + "rmdir should remove path: " + paths[i]); + } +}; + +exports.testMkpathTwice = function (assert) { + let dir = profilePath; + let path = file.join(dir, "test-file-dir"); + assert.ok(!file.exists(path), + "Sanity check: path should not exist: " + path); + file.mkpath(path); + assert.ok(file.exists(path), "mkpath should create path: " + path); + file.mkpath(path); + assert.ok(file.exists(path), + "After second mkpath, path should still exist: " + path); + file.rmdir(path); + assert.ok(!file.exists(path), "rmdir should remove path: " + path); +}; + +exports.testMkpathExistingNondirectory = function (assert) { + var fname = file.join(profilePath, 'conflict.txt'); + file.open(fname, "w").close(); + assert.ok(file.exists(fname), "File should exist"); + assert.throws(() => file.mkpath(fname), + /^The path already exists and is not a directory: .+$/, + "mkpath on file should raise error"); + file.remove(fname); +}; + +exports.testRmdirNondirectory = function (assert) { + var fname = file.join(profilePath, 'not-a-dir') + file.open(fname, "w").close(); + assert.ok(file.exists(fname), "File should exist"); + assert.throws(function() { + file.rmdir(fname); + }, ERRORS.NOT_A_DIRECTORY, "rmdir on file should raise error"); + file.remove(fname); + assert.ok(!file.exists(fname), "File should not exist"); + assert.throws(() => file.rmdir(fname), + ERRORS.FILE_NOT_FOUND, + "rmdir on non-existing file should raise error"); +}; + +exports.testRmdirNonempty = function (assert) { + let dir = profilePath; + let path = file.join(dir, "test-file-dir"); + assert.ok(!file.exists(path), + "Sanity check: path should not exist: " + path); + file.mkpath(path); + let filePath = file.join(path, "file"); + file.open(filePath, "w").close(); + assert.ok(file.exists(filePath), + "Sanity check: path should exist: " + filePath); + assert.throws(() => file.rmdir(path), + /^The directory is not empty: .+$/, + "rmdir on non-empty directory should raise error"); + file.remove(filePath); + file.rmdir(path); + assert.ok(!file.exists(path), "Path should not exist"); +}; + +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 new file mode 100644 index 000000000..501f93c87 --- /dev/null +++ b/addon-sdk/source/test/test-frame-utils.js @@ -0,0 +1,59 @@ +/* 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 { create } = require('sdk/frame/utils'); +const { open, close } = require('sdk/window/helpers'); + +exports['test frame creation'] = function(assert, done) { + open('data:text/html;charset=utf-8,Window').then(function (window) { + let frame = create(window.document); + + assert.equal(frame.getAttribute('type'), 'content', + 'frame type is content'); + assert.ok(frame.contentWindow, 'frame has contentWindow'); + assert.equal(frame.contentWindow.location.href, 'about:blank', + 'by default "about:blank" is loaded'); + assert.equal(frame.docShell.allowAuth, false, 'auth disabled by default'); + assert.equal(frame.docShell.allowJavascript, false, 'js disabled by default'); + assert.equal(frame.docShell.allowPlugins, false, + 'plugins disabled by default'); + close(window).then(done); + }); +}; + +exports['test fram has js disabled by default'] = function(assert, done) { + open('data:text/html;charset=utf-8,window').then(function (window) { + let frame = create(window.document, { + uri: 'data:text/html;charset=utf-8,', + }); + frame.contentWindow.addEventListener('DOMContentLoaded', function ready() { + frame.contentWindow.removeEventListener('DOMContentLoaded', ready, false); + assert.ok(!~frame.contentDocument.documentElement.innerHTML.indexOf('JS'), + 'JS was executed'); + + close(window).then(done); + }, false); + }); +}; + +exports['test frame with js enabled'] = function(assert, done) { + open('data:text/html;charset=utf-8,window').then(function (window) { + let frame = create(window.document, { + uri: 'data:text/html;charset=utf-8,', + allowJavascript: true + }); + frame.contentWindow.addEventListener('DOMContentLoaded', function ready() { + frame.contentWindow.removeEventListener('DOMContentLoaded', ready, false); + assert.ok(~frame.contentDocument.documentElement.innerHTML.indexOf('JS'), + 'JS was executed'); + + close(window).then(done); + }, false); + }); +}; + +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 000000000..442f71eda --- /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 000000000..0a55bcbf6 --- /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-fs.js b/addon-sdk/source/test/test-fs.js new file mode 100644 index 000000000..ed26ca3e3 --- /dev/null +++ b/addon-sdk/source/test/test-fs.js @@ -0,0 +1,621 @@ +/* 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 { pathFor, platform } = require("sdk/system"); +const fs = require("sdk/io/fs"); +const url = require("sdk/url"); +const path = require("sdk/fs/path"); +const { defer } = require("sdk/core/promise"); +const { Buffer } = require("sdk/io/buffer"); +const { is } = require("sdk/system/xul-app"); + +// Use profile directory to list / read / write files. +const profilePath = pathFor("ProfD"); +const fileNameInProfile = "compatibility.ini"; +const dirNameInProfile = "extensions"; +const filePathInProfile = path.join(profilePath, fileNameInProfile); +const dirPathInProfile = path.join(profilePath, dirNameInProfile); +const mkdirPath = path.join(profilePath, "sdk-fixture-mkdir"); +const writePath = path.join(profilePath, "sdk-fixture-writeFile"); +const unlinkPath = path.join(profilePath, "sdk-fixture-unlink"); +const truncatePath = path.join(profilePath, "sdk-fixture-truncate"); +const renameFromPath = path.join(profilePath, "sdk-fixture-rename-from"); +const renameToPath = path.join(profilePath, "sdk-fixture-rename-to"); +const chmodPath = path.join(profilePath, "sdk-fixture-chmod"); + +const profileEntries = [ + "compatibility.ini", + "extensions", + "prefs.js" + // There are likely to be a lot more files but we can"t really + // on consistent list so we limit to this. +]; + +const isWindows = platform.indexOf('win') === 0; + +exports["test readdir"] = function(assert, end) { + var async = false; + fs.readdir(profilePath, function(error, entries) { + assert.ok(async, "readdir is async"); + assert.ok(!error, "there is no error when reading directory"); + assert.ok(profileEntries.length <= entries.length, + "got at least number of entries we expect"); + assert.ok(profileEntries.every(function(entry) { + return entries.indexOf(entry) >= 0; + }), "all profiles are present"); + end(); + }); + + async = true; +}; + +exports["test readdir error"] = function(assert, end) { + var async = false; + var path = profilePath + "-does-not-exists"; + fs.readdir(path, function(error, entries) { + assert.ok(async, "readdir is async"); + assert.equal(error.message, "ENOENT, readdir " + path); + assert.equal(error.code, "ENOENT", "error has a code"); + assert.equal(error.path, path, "error has a path"); + assert.equal(error.errno, 34, "error has a errno"); + end(); + }); + + async = true; +}; + +exports["test readdirSync"] = function(assert) { + var async = false; + var entries = fs.readdirSync(profilePath); + assert.ok(profileEntries.length <= entries.length, + "got at least number of entries we expect"); + assert.ok(profileEntries.every(function(entry) { + return entries.indexOf(entry) >= 0; + }), "all profiles are present"); +}; + +exports["test readdirSync error"] = function(assert) { + var async = false; + var path = profilePath + "-does-not-exists"; + try { + fs.readdirSync(path); + assert.fail(Error("No error was thrown")); + } catch (error) { + assert.equal(error.message, "ENOENT, readdir " + path); + assert.equal(error.code, "ENOENT", "error has a code"); + assert.equal(error.path, path, "error has a path"); + assert.equal(error.errno, 34, "error has a errno"); + } +}; + +exports["test readFile"] = function(assert, end) { + let async = false; + fs.readFile(filePathInProfile, function(error, content) { + assert.ok(async, "readFile is async"); + assert.ok(!error, "error is falsy"); + + assert.ok(Buffer.isBuffer(content), "readFile returns buffer"); + assert.ok(typeof(content.length) === "number", "buffer has length"); + assert.ok(content.toString().indexOf("[Compatibility]") >= 0, + "content contains expected data"); + end(); + }); + async = true; +}; + +exports["test readFile error"] = function(assert, end) { + let async = false; + let path = filePathInProfile + "-does-not-exists"; + fs.readFile(path, function(error, content) { + assert.ok(async, "readFile is async"); + assert.equal(error.message, "ENOENT, open " + path); + assert.equal(error.code, "ENOENT", "error has a code"); + assert.equal(error.path, path, "error has a path"); + assert.equal(error.errno, 34, "error has a errno"); + + end(); + }); + async = true; +}; + +exports["test readFileSync not implemented"] = function(assert) { + let buffer = fs.readFileSync(filePathInProfile); + assert.ok(buffer.toString().indexOf("[Compatibility]") >= 0, + "read content"); +}; + +exports["test fs.stat file"] = function(assert, end) { + let async = false; + let path = filePathInProfile; + fs.stat(path, function(error, stat) { + assert.ok(async, "fs.stat is async"); + assert.ok(!error, "error is falsy"); + assert.ok(!stat.isDirectory(), "not a dir"); + assert.ok(stat.isFile(), "is a file"); + assert.ok(!stat.isSymbolicLink(), "isn't a symlink"); + assert.ok(typeof(stat.size) === "number", "size is a number"); + assert.ok(stat.exists === true, "file exists"); + assert.ok(typeof(stat.isBlockDevice()) === "boolean"); + assert.ok(typeof(stat.isCharacterDevice()) === "boolean"); + assert.ok(typeof(stat.isFIFO()) === "boolean"); + assert.ok(typeof(stat.isSocket()) === "boolean"); + assert.ok(typeof(stat.hidden) === "boolean"); + assert.ok(typeof(stat.writable) === "boolean") + assert.ok(stat.readable === true); + + end(); + }); + async = true; +}; + +exports["test fs.stat dir"] = function(assert, end) { + let async = false; + let path = dirPathInProfile; + fs.stat(path, function(error, stat) { + assert.ok(async, "fs.stat is async"); + assert.ok(!error, "error is falsy"); + assert.ok(stat.isDirectory(), "is a dir"); + assert.ok(!stat.isFile(), "not a file"); + assert.ok(!stat.isSymbolicLink(), "isn't a symlink"); + assert.ok(typeof(stat.size) === "number", "size is a number"); + assert.ok(stat.exists === true, "file exists"); + assert.ok(typeof(stat.isBlockDevice()) === "boolean"); + assert.ok(typeof(stat.isCharacterDevice()) === "boolean"); + assert.ok(typeof(stat.isFIFO()) === "boolean"); + assert.ok(typeof(stat.isSocket()) === "boolean"); + assert.ok(typeof(stat.hidden) === "boolean"); + assert.ok(typeof(stat.writable) === "boolean") + assert.ok(typeof(stat.readable) === "boolean"); + + end(); + }); + async = true; +}; + +exports["test fs.stat error"] = function(assert, end) { + let async = false; + let path = filePathInProfile + "-does-not-exists"; + fs.stat(path, function(error, stat) { + assert.ok(async, "fs.stat is async"); + assert.equal(error.message, "ENOENT, stat " + path); + assert.equal(error.code, "ENOENT", "error has a code"); + assert.equal(error.path, path, "error has a path"); + assert.equal(error.errno, 34, "error has a errno"); + + end(); + }); + async = true; +}; + +exports["test fs.exists NO"] = function(assert, end) { + let async = false + let path = filePathInProfile + "-does-not-exists"; + fs.exists(path, function(error, value) { + assert.ok(async, "fs.exists is async"); + assert.ok(!error, "error is falsy"); + assert.ok(!value, "file does not exists"); + end(); + }); + async = true; +}; + +exports["test fs.exists YES"] = function(assert, end) { + let async = false + let path = filePathInProfile + fs.exists(path, function(error, value) { + assert.ok(async, "fs.exists is async"); + assert.ok(!error, "error is falsy"); + assert.ok(value, "file exists"); + end(); + }); + async = true; +}; + +exports["test fs.exists NO"] = function(assert, end) { + let async = false + let path = filePathInProfile + "-does-not-exists"; + fs.exists(path, function(error, value) { + assert.ok(async, "fs.exists is async"); + assert.ok(!error, "error is falsy"); + assert.ok(!value, "file does not exists"); + end(); + }); + async = true; +}; + +exports["test fs.existsSync"] = function(assert) { + let path = filePathInProfile + assert.equal(fs.existsSync(path), true, "exists"); + assert.equal(fs.existsSync(path + "-does-not-exists"), false, "exists"); +}; + +exports["test fs.mkdirSync fs.rmdirSync"] = function(assert) { + let path = mkdirPath; + + assert.equal(fs.existsSync(path), false, "does not exists"); + fs.mkdirSync(path); + assert.equal(fs.existsSync(path), true, "dir was created"); + try { + fs.mkdirSync(path); + assert.fail(Error("mkdir on existing should throw")); + } catch (error) { + assert.equal(error.message, "EEXIST, mkdir " + path); + assert.equal(error.code, "EEXIST", "error has a code"); + assert.equal(error.path, path, "error has a path"); + assert.equal(error.errno, 47, "error has a errno"); + } + fs.rmdirSync(path); + assert.equal(fs.existsSync(path), false, "dir was removed"); +}; + +exports["test fs.mkdir"] = function(assert, end) { + let path = mkdirPath; + + if (!fs.existsSync(path)) { + let async = false; + fs.mkdir(path, function(error) { + assert.ok(async, "mkdir is async"); + assert.ok(!error, "no error"); + assert.equal(fs.existsSync(path), true, "dir was created"); + fs.rmdirSync(path); + assert.equal(fs.existsSync(path), false, "dir was removed"); + end(); + }); + async = true; + } +}; + +exports["test fs.mkdir error"] = function(assert, end) { + let path = mkdirPath; + + if (!fs.existsSync(path)) { + fs.mkdirSync(path); + let async = false; + fs.mkdir(path, function(error) { + assert.ok(async, "mkdir is async"); + assert.equal(error.message, "EEXIST, mkdir " + path); + assert.equal(error.code, "EEXIST", "error has a code"); + assert.equal(error.path, path, "error has a path"); + assert.equal(error.errno, 47, "error has a errno"); + fs.rmdirSync(path); + assert.equal(fs.existsSync(path), false, "dir was removed"); + end(); + }); + async = true; + } +}; + +exports["test fs.rmdir"] = function(assert, end) { + let path = mkdirPath; + + if (!fs.existsSync(path)) { + fs.mkdirSync(path); + assert.equal(fs.existsSync(path), true, "dir exists"); + let async = false; + fs.rmdir(path, function(error) { + assert.ok(async, "mkdir is async"); + assert.ok(!error, "no error"); + assert.equal(fs.existsSync(path), false, "dir was removed"); + end(); + }); + async = true; + } +}; + + +exports["test fs.rmdir error"] = function(assert, end) { + let path = mkdirPath; + + if (!fs.existsSync(path)) { + assert.equal(fs.existsSync(path), false, "dir doesn't exists"); + let async = false; + fs.rmdir(path, function(error) { + assert.ok(async, "mkdir is async"); + assert.equal(error.message, "ENOENT, remove " + path); + assert.equal(error.code, "ENOENT", "error has a code"); + assert.equal(error.path, path, "error has a path"); + assert.equal(error.errno, 34, "error has a errno"); + assert.equal(fs.existsSync(path), false, "dir is removed"); + end(); + }); + async = true; + } +}; + +exports["test fs.truncateSync fs.unlinkSync"] = function(assert) { + let path = truncatePath; + + assert.equal(fs.existsSync(path), false, "does not exists"); + fs.truncateSync(path); + assert.equal(fs.existsSync(path), true, "file was created"); + fs.truncateSync(path); + fs.unlinkSync(path); + assert.equal(fs.existsSync(path), false, "file was removed"); +}; + + +exports["test fs.truncate"] = function(assert, end) { + let path = truncatePath; + if (!fs.existsSync(path)) { + let async = false; + fs.truncate(path, 0, function(error) { + assert.ok(async, "truncate is async"); + assert.ok(!error, "no error"); + assert.equal(fs.existsSync(path), true, "file was created"); + fs.unlinkSync(path); + assert.equal(fs.existsSync(path), false, "file was removed"); + end(); + }) + async = true; + } +}; + +exports["test fs.unlink"] = function(assert, end) { + let path = unlinkPath; + let async = false; + assert.ok(!fs.existsSync(path), "file doesn't exists yet"); + fs.truncateSync(path, 0); + assert.ok(fs.existsSync(path), "file was created"); + fs.unlink(path, function(error) { + assert.ok(async, "fs.unlink is async"); + assert.ok(!error, "error is falsy"); + assert.ok(!fs.existsSync(path), "file was removed"); + end(); + }); + async = true; +}; + +exports["test fs.unlink error"] = function(assert, end) { + let path = unlinkPath; + let async = false; + assert.ok(!fs.existsSync(path), "file doesn't exists yet"); + fs.unlink(path, function(error) { + assert.ok(async, "fs.unlink is async"); + assert.equal(error.message, "ENOENT, remove " + path); + assert.equal(error.code, "ENOENT", "error has a code"); + assert.equal(error.path, path, "error has a path"); + assert.equal(error.errno, 34, "error has a errno"); + end(); + }); + async = true; +}; + +exports["test fs.rename"] = function(assert, end) { + let fromPath = renameFromPath; + let toPath = renameToPath; + + fs.truncateSync(fromPath); + assert.ok(fs.existsSync(fromPath), "source file exists"); + assert.ok(!fs.existsSync(toPath), "destination doesn't exists"); + var async = false; + fs.rename(fromPath, toPath, function(error) { + assert.ok(async, "fs.rename is async"); + assert.ok(!error, "error is falsy"); + assert.ok(!fs.existsSync(fromPath), "source path no longer exists"); + assert.ok(fs.existsSync(toPath), "destination file exists"); + fs.unlinkSync(toPath); + assert.ok(!fs.existsSync(toPath), "cleaned up properly"); + end(); + }); + async = true; +}; + +exports["test fs.rename (missing source file)"] = function(assert, end) { + let fromPath = renameFromPath; + let toPath = renameToPath; + + assert.ok(!fs.existsSync(fromPath), "source file doesn't exists"); + assert.ok(!fs.existsSync(toPath), "destination doesn't exists"); + var async = false; + fs.rename(fromPath, toPath, function(error) { + assert.ok(async, "fs.rename is async"); + assert.equal(error.message, "ENOENT, rename " + fromPath); + assert.equal(error.code, "ENOENT", "error has a code"); + assert.equal(error.path, fromPath, "error has a path"); + assert.equal(error.errno, 34, "error has a errno"); + end(); + }); + async = true; +}; + +exports["test fs.rename (existing target file)"] = function(assert, end) { + let fromPath = renameFromPath; + let toPath = renameToPath; + + fs.truncateSync(fromPath); + fs.truncateSync(toPath); + assert.ok(fs.existsSync(fromPath), "source file exists"); + assert.ok(fs.existsSync(toPath), "destination file exists"); + var async = false; + fs.rename(fromPath, toPath, function(error) { + assert.ok(async, "fs.rename is async"); + assert.ok(!error, "error is falsy"); + assert.ok(!fs.existsSync(fromPath), "source path no longer exists"); + assert.ok(fs.existsSync(toPath), "destination file exists"); + fs.unlinkSync(toPath); + assert.ok(!fs.existsSync(toPath), "cleaned up properly"); + end(); + }); + async = true; +}; + +exports["test fs.writeFile"] = function(assert, end) { + let path = writePath; + let content = ["hello world", + "this is some text"].join("\n"); + + var async = false; + fs.writeFile(path, content, function(error) { + assert.ok(async, "fs write is async"); + assert.ok(!error, "error is falsy"); + assert.ok(fs.existsSync(path), "file was created"); + assert.equal(fs.readFileSync(path).toString(), + content, + "contet was written"); + fs.unlinkSync(path); + assert.ok(!fs.exists(path), "file was removed"); + + end(); + }); + async = true; +}; + +exports["test fs.writeFile (with large files)"] = function(assert, end) { + let path = writePath; + let content = ""; + + for (var i = 0; i < 100000; i++) { + content += "buffer\n"; + } + + var async = false; + fs.writeFile(path, content, function(error) { + assert.ok(async, "fs write is async"); + assert.ok(!error, "error is falsy"); + assert.ok(fs.existsSync(path), "file was created"); + assert.equal(fs.readFileSync(path).toString(), + content, + "contet was written"); + fs.unlinkSync(path); + assert.ok(!fs.exists(path), "file was removed"); + + end(); + }); + async = true; +}; + +exports["test fs.writeFile error"] = function (assert, done) { + try { + fs.writeFile({}, 'content', function (err) { + assert.fail('Error thrown from TypeError should not be caught'); + }); + } catch (e) { + assert.ok(e, + 'writeFile with a non-string error should not be caught'); + assert.equal(e.name, 'TypeError', 'error should be TypeError'); + } + fs.writeFile('not/a/valid/path', 'content', function (err) { + assert.ok(err, 'error caught and handled in callback'); + done(); + }); +}; + +exports["test fs.chmod"] = function (assert, done) { + let content = ["hej från sverige"]; + + fs.writeFile(chmodPath, content, function (err) { + testPerm("0755")() + .then(testPerm("0777")) + .then(testPerm("0666")) + .then(testPerm(0o511)) + .then(testPerm(0o200)) + .then(testPerm("0040")) + .then(testPerm("0000")) + .then(testPermSync(0o777)) + .then(testPermSync(0o666)) + .then(testPermSync("0511")) + .then(testPermSync("0200")) + .then(testPermSync("0040")) + .then(testPermSync("0000")) + .then(() => { + assert.pass("Successful chmod passes"); + }, assert.fail) + // Test invalid paths + .then(() => chmod("not-a-valid-file", 0o755)) + .then(assert.fail, (err) => { + checkPermError(err, "not-a-valid-file"); + }) + .then(() => chmod("not-a-valid-file", 0o755, "sync")) + .then(assert.fail, (err) => { + checkPermError(err, "not-a-valid-file"); + }) + // Test invalid files + .then(() => chmod("resource://not-a-real-file", 0o755)) + .then(assert.fail, (err) => { + checkPermError(err, "resource://not-a-real-file"); + }) + .then(() => chmod("resource://not-a-real-file", 0o755, 'sync')) + .then(assert.fail, (err) => { + checkPermError(err, "resource://not-a-real-file"); + }) + .then(done, assert.fail); + }); + + function checkPermError (err, path) { + assert.equal(err.message, "ENOENT, chmod " + path); + assert.equal(err.code, "ENOENT", "error has a code"); + assert.equal(err.path, path, "error has a path"); + assert.equal(err.errno, 34, "error has a errno"); + } + + function chmod (path, mode, sync) { + let { promise, resolve, reject } = defer(); + if (!sync) { + fs.chmod(path, mode, (err) => { + if (err) reject(err); + else resolve(); + }); + } else { + fs.chmodSync(path, mode); + resolve(); + } + return promise; + } + + function testPerm (mode, sync) { + return function () { + return chmod(chmodPath, mode, sync) + .then(() => getPerm(chmodPath)) + .then(perm => { + let nMode = normalizeMode(mode); + if (isWindows) + assert.equal(perm, nMode, + "mode correctly set to " + mode + " (" + nMode + " on windows)"); + else + assert.equal(perm, nMode, "mode correctly set to " + nMode); + }); + }; + } + + function testPermSync (mode) { + return testPerm(mode, true); + } + + function getPerm (path) { + let { promise, resolve, reject } = defer(); + fs.stat(path, function (err, stat) { + if (err) reject(err); + else resolve(stat.mode); + }); + return promise; + } + + /* + * Converts a unix mode `0755` into a Windows version of unix permissions + */ + function normalizeMode (mode) { + if (typeof mode === "string") + mode = parseInt(mode, 8); + + if (!isWindows) + return mode; + + var ANY_READ = 0o444; + var ANY_WRITE = 0o222; + var winMode = 0; + + // On Windows, if WRITE is true, then READ is also true + if (mode & ANY_WRITE) + winMode |= ANY_WRITE | ANY_READ; + // Minimum permissions are READ for Windows + else + winMode |= ANY_READ; + + return winMode; + } +}; + +require("test").run(exports); diff --git a/addon-sdk/source/test/test-functional.js b/addon-sdk/source/test/test-functional.js new file mode 100644 index 000000000..02ae15fc6 --- /dev/null +++ b/addon-sdk/source/test/test-functional.js @@ -0,0 +1,463 @@ +/* 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 { setTimeout } = require('sdk/timers'); +const utils = require('sdk/lang/functional'); +const { invoke, defer, partial, compose, memoize, once, is, isnt, + delay, wrap, curry, chainable, field, query, isInstance, debounce, throttle +} = utils; +const { LoaderWithHookedConsole } = require('sdk/test/loader'); + +exports['test forwardApply'] = function(assert) { + function sum(b, c) { return this.a + b + c; } + assert.equal(invoke(sum, [2, 3], { a: 1 }), 6, + 'passed arguments and pseoude-variable are used'); + + assert.equal(invoke(sum.bind({ a: 2 }), [2, 3], { a: 1 }), 7, + 'bounded `this` pseoudo variable is used'); +}; + +exports['test deferred function'] = function(assert, done) { + let nextTurn = false; + function sum(b, c) { + assert.ok(nextTurn, 'enqueued is called in next turn of event loop'); + assert.equal(this.a + b + c, 6, + 'passed arguments an pseoude-variable are used'); + done(); + } + + let fixture = { a: 1, method: defer(sum) }; + fixture.method(2, 3); + nextTurn = true; +}; + +exports['test partial function'] = function(assert) { + function sum(b, c) { return this.a + b + c; } + + let foo = { a : 5 }; + + foo.sum7 = partial(sum, 7); + foo.sum8and4 = partial(sum, 8, 4); + + assert.equal(foo.sum7(2), 14, 'partial one arguments works'); + + assert.equal(foo.sum8and4(), 17, 'partial both arguments works'); +}; + +exports["test curry defined numeber of arguments"] = function(assert) { + var sum = curry(function(a, b, c) { + return a + b + c; + }); + + assert.equal(sum(2, 2, 1), 5, "sum(2, 2, 1) => 5"); + assert.equal(sum(2, 4)(1), 7, "sum(2, 4)(1) => 7"); + assert.equal(sum(2)(4, 2), 8, "sum(2)(4, 2) => 8"); + assert.equal(sum(2)(4)(3), 9, "sum(2)(4)(3) => 9"); +}; + +exports['test compose'] = function(assert) { + let greet = function(name) { return 'hi: ' + name; }; + let exclaim = function(sentence) { return sentence + '!'; }; + + assert.equal(compose(exclaim, greet)('moe'), 'hi: moe!', + 'can compose a function that takes another'); + + assert.equal(compose(greet, exclaim)('moe'), 'hi: moe!', + 'in this case, the functions are also commutative'); + + let target = { + name: 'Joe', + greet: compose(function exclaim(sentence) { + return sentence + '!'; + }, function(title) { + return 'hi : ' + title + ' ' + this.name; + }) + }; + + assert.equal(target.greet('Mr'), 'hi : Mr Joe!', + 'this can be passed in'); + assert.equal(target.greet.call({ name: 'Alex' }, 'Dr'), 'hi : Dr Alex!', + 'this can be applied'); + + let single = compose(function(value) { + return value + ':suffix'; + }); + + assert.equal(single('text'), 'text:suffix', 'works with single function'); + + let identity = compose(); + assert.equal(identity('bla'), 'bla', 'works with zero functions'); +}; + +exports['test wrap'] = function(assert) { + let greet = function(name) { return 'hi: ' + name; }; + let backwards = wrap(greet, function(f, name) { + return f(name) + ' ' + name.split('').reverse().join(''); + }); + + assert.equal(backwards('moe'), 'hi: moe eom', + 'wrapped the saluation function'); + + let inner = function () { return 'Hello '; }; + let target = { + name: 'Matteo', + hi: wrap(inner, function(f) { return f() + this.name; }) + }; + + assert.equal(target.hi(), 'Hello Matteo', 'works with this'); + + function noop() { } + let wrapped = wrap(noop, function(f) { + return Array.slice(arguments); + }); + + let actual = wrapped([ 'whats', 'your' ], 'vector', 'victor'); + assert.deepEqual(actual, [ noop, ['whats', 'your'], 'vector', 'victor' ], + 'works with fancy stuff'); +}; + +exports['test memoize'] = function(assert) { + const fib = n => n < 2 ? n : fib(n - 1) + fib(n - 2); + let fibnitro = memoize(fib); + + assert.equal(fib(10), 55, + 'a memoized version of fibonacci produces identical results'); + assert.equal(fibnitro(10), 55, + 'a memoized version of fibonacci produces identical results'); + + function o(key, value) { return value; } + let oo = memoize(o), v1 = {}, v2 = {}; + + + assert.equal(oo(1, v1), v1, 'returns value back'); + assert.equal(oo(1, v2), v1, 'memoized by a first argument'); + assert.equal(oo(2, v2), v2, 'returns back value if not memoized'); + assert.equal(oo(2), v2, 'memoized new value'); + assert.notEqual(oo(1), oo(2), 'values do not override'); + assert.equal(o(3, v2), oo(2, 3), 'returns same value as un-memoized'); + + let get = memoize(function(attribute) { return this[attribute]; }); + let target = { name: 'Bob', get: get }; + + assert.equal(target.get('name'), 'Bob', 'has correct `this`'); + assert.equal(target.get.call({ name: 'Jack' }, 'name'), 'Bob', + 'name is memoized'); + assert.equal(get('name'), 'Bob', 'once memoized can be called without this'); +}; + +exports['test delay'] = function(assert, done) { + let delayed = false; + delay(function() { + assert.ok(delayed, 'delayed the function'); + done(); + }, 1); + delayed = true; +}; + +exports['test delay with this'] = function(assert, done) { + let context = {}; + delay.call(context, function(name) { + assert.equal(this, context, 'this was passed in'); + assert.equal(name, 'Tom', 'argument was passed in'); + done(); + }, 10, 'Tom'); +}; + +exports['test once'] = function(assert) { + let n = 0; + let increment = once(function() { n ++; }); + + increment(); + increment(); + + assert.equal(n, 1, 'only incremented once'); + + let target = { + state: 0, + update: once(function() { + return this.state ++; + }) + }; + + target.update(); + target.update(); + + assert.equal(target.state, 1, 'this was passed in and called only once'); +}; + +exports['test once with argument'] = function(assert) { + let n = 0; + let increment = once(a => n++); + + increment(); + increment('foo'); + + assert.equal(n, 1, 'only incremented once'); + + increment(); + increment('foo'); + + assert.equal(n, 1, 'only incremented once'); +}; + +exports['test complement'] = assert => { + let { complement } = require("sdk/lang/functional"); + + let isOdd = x => Boolean(x % 2); + + assert.equal(isOdd(1), true); + assert.equal(isOdd(2), false); + + let isEven = complement(isOdd); + + assert.equal(isEven(1), false); + assert.equal(isEven(2), true); + + let foo = {}; + let isFoo = function() { return this === foo; }; + let insntFoo = complement(isFoo); + + assert.equal(insntFoo.call(foo), false); + assert.equal(insntFoo.call({}), true); +}; + +exports['test constant'] = assert => { + let { constant } = require("sdk/lang/functional"); + + let one = constant(1); + + assert.equal(one(1), 1); + assert.equal(one(2), 1); +}; + +exports['test apply'] = assert => { + let { apply } = require("sdk/lang/functional"); + + let dashify = (...args) => args.join("-"); + + assert.equal(apply(dashify, 1, [2, 3]), "1-2-3"); + assert.equal(apply(dashify, "a"), "a"); + assert.equal(apply(dashify, ["a", "b"]), "a-b"); + assert.equal(apply(dashify, ["a", "b"], "c"), "a,b-c"); + assert.equal(apply(dashify, [1, 2], [3, 4]), "1,2-3-4"); +}; + +exports['test flip'] = assert => { + let { flip } = require("sdk/lang/functional"); + + let append = (left, right) => left + " " + right; + let prepend = flip(append); + + assert.equal(append("hello", "world"), "hello world"); + assert.equal(prepend("hello", "world"), "world hello"); + + let wrap = function(left, right) { + return left + " " + this + " " + right; + }; + let invertWrap = flip(wrap); + + assert.equal(wrap.call("@", "hello", "world"), "hello @ world"); + assert.equal(invertWrap.call("@", "hello", "world"), "world @ hello"); + + let reverse = flip((...args) => args); + + assert.deepEqual(reverse(1, 2, 3, 4), [4, 3, 2, 1]); + assert.deepEqual(reverse(1), [1]); + assert.deepEqual(reverse(), []); + + // currying still works + let prependr = curry(prepend); + + assert.equal(prependr("hello", "world"), "world hello"); + assert.equal(prependr("hello")("world"), "world hello"); +}; + +exports["test when"] = assert => { + let { when } = require("sdk/lang/functional"); + + let areNums = (...xs) => xs.every(x => typeof(x) === "number"); + + let sum = when(areNums, (...xs) => xs.reduce((y, x) => x + y, 0)); + + assert.equal(sum(1, 2, 3), 6); + assert.equal(sum(1, 2, "3"), undefined); + + let multiply = when(areNums, + (...xs) => xs.reduce((y, x) => x * y, 1), + (...xs) => xs); + + assert.equal(multiply(2), 2); + assert.equal(multiply(2, 3), 6); + assert.deepEqual(multiply(2, "4"), [2, "4"]); + + function Point(x, y) { + this.x = x; + this.y = y; + } + + let isPoint = x => x instanceof Point; + + let inc = when(isPoint, ({x, y}) => new Point(x + 1, y + 1)); + + assert.equal(inc({}), undefined); + assert.deepEqual(inc(new Point(0, 0)), { x: 1, y: 1 }); + + let axis = when(isPoint, + ({ x, y }) => [x, y], + _ => [0, 0]); + + assert.deepEqual(axis(new Point(1, 4)), [1, 4]); + assert.deepEqual(axis({ foo: "bar" }), [0, 0]); +}; + +exports["test chainable"] = function(assert) { + let Player = function () { this.volume = 5; }; + Player.prototype = { + setBand: chainable(function (band) { return (this.band = band); }), + incVolume: chainable(function () { return this.volume++; }) + }; + let player = new Player(); + player + .setBand('Animals As Leaders') + .incVolume().incVolume().incVolume().incVolume().incVolume().incVolume(); + + assert.equal(player.band, 'Animals As Leaders', 'passes arguments into chained'); + assert.equal(player.volume, 11, 'accepts no arguments in chain'); +}; + +exports["test field"] = assert => { + let Num = field("constructor", 0); + assert.equal(Num.name, Number.name); + assert.ok(typeof(Num), "function"); + + let x = field("x"); + + [ + [field("foo", { foo: 1 }), 1], + [field("foo")({ foo: 1 }), 1], + [field("bar", {}), undefined], + [field("bar")({}), undefined], + [field("hey", undefined), undefined], + [field("hey")(undefined), undefined], + [field("how", null), null], + [field("how")(null), null], + [x(1), undefined], + [x(undefined), undefined], + [x(null), null], + [x({ x: 1 }), 1], + [x({ x: 2 }), 2], + ].forEach(([actual, expected]) => assert.equal(actual, expected)); +}; + +exports["test query"] = assert => { + let Num = query("constructor", 0); + assert.equal(Num.name, Number.name); + assert.ok(typeof(Num), "function"); + + let x = query("x"); + let xy = query("x.y"); + + [ + [query("foo", { foo: 1 }), 1], + [query("foo")({ foo: 1 }), 1], + [query("foo.bar", { foo: { bar: 2 } }), 2], + [query("foo.bar")({ foo: { bar: 2 } }), 2], + [query("foo.bar", { foo: 1 }), undefined], + [query("foo.bar")({ foo: 1 }), undefined], + [x(1), undefined], + [x(undefined), undefined], + [x(null), null], + [x({ x: 1 }), 1], + [x({ x: 2 }), 2], + [xy(1), undefined], + [xy(undefined), undefined], + [xy(null), null], + [xy({ x: 1 }), undefined], + [xy({ x: 2 }), undefined], + [xy({ x: { y: 1 } }), 1], + [xy({ x: { y: 2 } }), 2] + ].forEach(([actual, expected]) => assert.equal(actual, expected)); +}; + +exports["test isInstance"] = assert => { + function X() {} + function Y() {} + let isX = isInstance(X); + + [ + isInstance(X, new X()), + isInstance(X)(new X()), + !isInstance(X, new Y()), + !isInstance(X)(new Y()), + isX(new X()), + !isX(new Y()) + ].forEach(x => assert.ok(x)); +}; + +exports["test is"] = assert => { + + assert.deepEqual([ 1, 0, 1, 0, 1 ].map(is(1)), + [ true, false, true, false, true ], + "is can be partially applied"); + + assert.ok(is(1, 1)); + assert.ok(!is({}, {})); + assert.ok(is()(1)()(1), "is is curried"); + assert.ok(!is()(1)()(2)); +}; + +exports["test isnt"] = assert => { + + assert.deepEqual([ 1, 0, 1, 0, 1 ].map(isnt(0)), + [ true, false, true, false, true ], + "is can be partially applied"); + + assert.ok(!isnt(1, 1)); + assert.ok(isnt({}, {})); + assert.ok(!isnt()(1)()(1)); + assert.ok(isnt()(1)()(2)); +}; + +exports["test debounce"] = (assert, done) => { + let counter = 0; + let fn = debounce(() => counter++, 100); + + new Array(10).join(0).split("").forEach(fn); + + assert.equal(counter, 0, "debounce does not fire immediately"); + setTimeout(() => { + assert.equal(counter, 1, "function called after wait time"); + fn(); + setTimeout(() => { + assert.equal(counter, 2, "function able to be called again after wait"); + done(); + }, 150); + }, 200); +}; + +exports["test throttle"] = (assert, done) => { + let called = 0; + let attempt = 0; + let atleast100ms = false; + let throttledFn = throttle(() => { + called++; + if (called === 2) { + assert.equal(attempt, 10, "called twice, but attempted 10 times"); + fn(); + } + if (called === 3) { + assert.ok(atleast100ms, "atleast 100ms have passed"); + assert.equal(attempt, 11, "called third, waits for delay to happen"); + done(); + } + }, 200); + let fn = () => ++attempt && throttledFn(); + + setTimeout(() => atleast100ms = true, 100); + + new Array(11).join(0).split("").forEach(fn); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-globals.js b/addon-sdk/source/test/test-globals.js new file mode 100644 index 000000000..bc3364367 --- /dev/null +++ b/addon-sdk/source/test/test-globals.js @@ -0,0 +1,30 @@ +/* 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'; + +Object.defineProperty(this, 'global', { value: this }); + +exports.testGlobals = function(assert) { + // the only globals in module scope should be: + // module, exports, require, dump, console + assert.equal(typeof module, 'object', 'have "module", good'); + assert.equal(typeof exports, 'object', 'have "exports", good'); + assert.equal(typeof require, 'function', 'have "require", good'); + assert.equal(typeof dump, 'function', 'have "dump", good'); + assert.equal(typeof console, 'object', 'have "console", good'); + + // in particular, these old globals should no longer be present + assert.ok(!('packaging' in global), "no 'packaging', good"); + assert.ok(!('memory' in global), "no 'memory', good"); + assert.ok(/test-globals\.js$/.test(module.uri), + 'should contain filename'); +}; + +exports.testComponent = function (assert) { + assert.throws(() => { + Components; + }, /`Components` is not available/, 'using `Components` throws'); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-heritage.js b/addon-sdk/source/test/test-heritage.js new file mode 100644 index 000000000..e087f3e4d --- /dev/null +++ b/addon-sdk/source/test/test-heritage.js @@ -0,0 +1,301 @@ +/* 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'); + +exports['test extend'] = function(assert) { + let ancestor = { a: 1 }; + let descendant = extend(ancestor, { + b: 2, + get c() { return 3 }, + d: function() { return 4 } + }); + + assert.ok(ancestor.isPrototypeOf(descendant), + 'descendant inherits from ancestor'); + assert.ok(descendant.b, 2, 'proprety was implemented'); + assert.ok(descendant.c, 3, 'getter was implemented'); + assert.ok(descendant.d(), 4, 'method was implemented'); + + /* Will be fixed once Bug 674195 is shipped. + assert.ok(Object.isFrozen(descendant), + 'extend returns frozen objects'); + */ +}; + +exports['test mix'] = function(assert) { + let ancestor = { a: 1 } + let mixed = mix(extend(ancestor, { b: 1, c: 1 }), { c: 2 }, { d: 3 }); + + assert.deepEqual(JSON.parse(JSON.stringify(mixed)), { b: 1, c: 2, d: 3 }, + 'properties mixed as expected'); + assert.ok(ancestor.isPrototypeOf(mixed), + 'first arguments ancestor is ancestor of result'); +}; + +exports['test obscure'] = function(assert) { + let fixture = mix({ a: 1 }, obscure({ b: 2 })); + + assert.equal(fixture.a, 1, 'a property is included'); + assert.equal(fixture.b, 2, 'b proprety is included'); + assert.ok(!Object.getOwnPropertyDescriptor(fixture, 'b').enumerable, + 'obscured properties are non-enumerable'); +}; + +exports['test inheritance'] = function(assert) { + let Ancestor = Class({ + name: 'ancestor', + method: function () { + return 'hello ' + this.name; + } + }); + + assert.ok(Ancestor() instanceof Ancestor, + 'can be instantiated without new'); + assert.ok(new Ancestor() instanceof Ancestor, + 'can also be instantiated with new'); + assert.ok(Ancestor() instanceof Class, + 'if ancestor not specified than defaults to Class'); + assert.ok(Ancestor.prototype.extends, Class.prototype, + 'extends of prototype points to ancestors prototype'); + + + assert.equal(Ancestor().method(), 'hello ancestor', + 'instance inherits defined properties'); + + let Descendant = Class({ + extends: Ancestor, + name: 'descendant' + }); + + assert.ok(Descendant() instanceof Descendant, + 'instantiates correctly'); + assert.ok(Descendant() instanceof Ancestor, + 'Inherits for passed `extends`'); + assert.equal(Descendant().method(), 'hello descendant', + 'propreties inherited'); +}; + +exports['test immunity against __proto__'] = function(assert) { + let Foo = Class({ name: 'foo', hacked: false }); + + let Bar = Class({ extends: Foo, name: 'bar' }); + + assert.throws(function() { + Foo.prototype.__proto__ = { hacked: true }; + if (Foo() instanceof Base && !Foo().hacked) + throw Error('can not change prototype chain'); + }, 'prototype chain is immune to __proto__ hacks'); + + assert.throws(function() { + Foo.prototype.__proto__ = { hacked: true }; + if (Bar() instanceof Foo && !Bar().hacked) + throw Error('can not change prototype chain'); + }, 'prototype chain of decedants immune to __proto__ hacks'); +}; + +exports['test super'] = function(assert) { + var Foo = Class({ + initialize: function initialize(options) { + this.name = options.name; + } + }); + + var Bar = Class({ + extends: Foo, + initialize: function Bar(options) { + Foo.prototype.initialize.call(this, options); + this.type = 'bar'; + } + }); + + var bar = Bar({ name: 'test' }); + + assert.equal(bar.type, 'bar', 'bar initializer was called'); + assert.equal(bar.name, 'test', 'bar initializer called Foo initializer'); +}; + +exports['test initialize'] = function(assert) { + var Dog = Class({ + initialize: function initialize(name) { + this.name = name; + }, + type: 'dog', + bark: function bark() { + return 'Ruff! Ruff!' + } + }); + + var fluffy = Dog('Fluffy'); // instatiation + assert.ok(fluffy instanceof Dog, + 'instanceof works as expected'); + assert.ok(fluffy instanceof Class, + 'inherits form Class if not specified otherwise'); + assert.ok(fluffy.name, 'fluffy', + 'initialize unless specified otherwise'); +}; + +exports['test complements regular inheritace'] = function(assert) { + let Base = Class({ name: 'base' }); + + function Type() { + // ... + } + Type.prototype = Object.create(Base.prototype); + Type.prototype.run = function() { + // ... + }; + + let value = new Type(); + + assert.ok(value instanceof Type, 'creates instance of Type'); + assert.ok(value instanceof Base, 'inherits from Base'); + assert.equal(value.name, 'base', 'inherits properties from Base'); + + + let SubType = Class({ + extends: Type, + sub: 'type' + }); + + let fixture = SubType(); + + assert.ok(fixture instanceof Base, 'is instance of Base'); + assert.ok(fixture instanceof Type, 'is instance of Type'); + assert.ok(fixture instanceof SubType, 'is instance of SubType'); + + assert.equal(fixture.sub, 'type', 'proprety is defined'); + assert.equal(fixture.run, Type.prototype.run, 'proprety is inherited'); + assert.equal(fixture.name, 'base', 'inherits base properties'); +}; + +exports['test extends object'] = function(assert) { + let prototype = { constructor: function() { return this; }, name: 'me' }; + let Foo = Class({ + extends: prototype, + value: 2 + }); + let foo = new Foo(); + + assert.ok(foo instanceof Foo, 'instance of Foo'); + assert.ok(!(foo instanceof Class), 'is not instance of Class'); + assert.ok(prototype.isPrototypeOf(foo), 'inherits from given prototype'); + assert.equal(Object.getPrototypeOf(Foo.prototype), prototype, + 'contsructor prototype inherits from extends option'); + assert.equal(foo.value, 2, 'property is defined'); + assert.equal(foo.name, 'me', 'prototype proprety is inherited'); +}; + + +var HEX = Class({ + hex: function hex() { + return '#' + this.color; + } +}); + +var RGB = Class({ + red: function red() { + return parseInt(this.color.substr(0, 2), 16); + }, + green: function green() { + return parseInt(this.color.substr(2, 2), 16); + }, + blue: function blue() { + return parseInt(this.color.substr(4, 2), 16); + } +}); + +var CMYK = Class({ + black: function black() { + var color = Math.max(Math.max(this.red(), this.green()), this.blue()); + return (1 - color / 255).toFixed(4); + }, + magenta: function magenta() { + var K = this.black(); + return (((1 - this.green() / 255).toFixed(4) - K) / (1 - K)).toFixed(4); + }, + yellow: function yellow() { + var K = this.black(); + return (((1 - this.blue() / 255).toFixed(4) - K) / (1 - K)).toFixed(4); + }, + cyan: function cyan() { + var K = this.black(); + return (((1 - this.red() / 255).toFixed(4) - K) / (1 - K)).toFixed(4); + } +}); + +var Color = Class({ + implements: [ HEX, RGB, CMYK ], + initialize: function initialize(color) { + this.color = color; + } +}); + +exports['test composition'] = function(assert) { + var pink = Color('FFC0CB'); + + assert.equal(pink.red(), 255, 'red() works'); + assert.equal(pink.green(), 192, 'green() works'); + assert.equal(pink.blue(), 203, 'blue() works'); + + assert.equal(pink.magenta(), 0.2471, 'magenta() works'); + assert.equal(pink.yellow(), 0.2039, 'yellow() works'); + assert.equal(pink.cyan(), 0.0000, 'cyan() works'); + + assert.ok(pink instanceof Color, 'is instance of Color'); + assert.ok(pink instanceof Class, 'is instance of Class'); +}; + +var Point = Class({ + initialize: function initialize(x, y) { + this.x = x; + this.y = y; + }, + toString: function toString() { + return this.x + ':' + this.y; + } +}) + +var Pixel = Class({ + extends: Point, + implements: [ Color ], + initialize: function initialize(x, y, color) { + Color.prototype.initialize.call(this, color); + Point.prototype.initialize.call(this, x, y); + }, + toString: function toString() { + return this.hex() + '@' + Point.prototype.toString.call(this) + } +}); + +exports['test compostion with inheritance'] = function(assert) { + var pixel = Pixel(11, 23, 'CC3399'); + + assert.equal(pixel.toString(), '#CC3399@11:23', 'stringifies correctly'); + assert.ok(pixel instanceof Pixel, 'instance of Pixel'); + assert.ok(pixel instanceof Point, 'instance of Point'); +}; + +exports['test composition with objects'] = function(assert) { + var A = { a: 1, b: 1 }; + var B = Class({ b: 2, c: 2 }); + var C = { c: 3 }; + var D = { d: 4 }; + + var ABCD = Class({ + implements: [ A, B, C, D ], + e: 5 + }); + + var f = ABCD(); + + assert.equal(f.a, 1, 'inherits A.a'); + assert.equal(f.b, 2, 'inherits B.b overrides A.b'); + assert.equal(f.c, 3, 'inherits C.c overrides B.c'); + assert.equal(f.d, 4, 'inherits D.d'); + assert.equal(f.e, 5, 'implements e'); +}; + +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 new file mode 100644 index 000000000..945c2413f --- /dev/null +++ b/addon-sdk/source/test/test-hidden-frame.js @@ -0,0 +1,71 @@ +/* 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("sdk/test/loader"); +const hiddenFrames = require("sdk/frame/hidden-frame"); +const { HiddenFrame } = hiddenFrames; + +exports["test Frame"] = function(assert, done) { + let url = "data:text/html;charset=utf-8,"; + + let hiddenFrame = hiddenFrames.add(HiddenFrame({ + onReady: function () { + assert.equal(this.element.contentWindow.location, "about:blank", + "HiddenFrame loads about:blank by default."); + + function onDOMReady() { + hiddenFrame.element.removeEventListener("DOMContentLoaded", onDOMReady, + false); + assert.equal(hiddenFrame.element.contentWindow.location, url, + "HiddenFrame loads the specified content."); + done(); + } + + this.element.addEventListener("DOMContentLoaded", onDOMReady, false); + this.element.setAttribute("src", url); + } + })); +}; + +exports["test frame removed properly"] = function(assert, done) { + let url = "data:text/html;charset=utf-8,"; + + let hiddenFrame = hiddenFrames.add(HiddenFrame({ + onReady: function () { + let frame = this.element; + assert.ok(frame.parentNode, "frame has a parent node"); + hiddenFrames.remove(hiddenFrame); + assert.ok(!frame.parentNode, "frame no longer has a parent node"); + done(); + } + })); +}; + +exports["test unload detaches panels"] = function(assert, done) { + let loader = Loader(module); + let { add, remove, HiddenFrame } = loader.require("sdk/frame/hidden-frame"); + let frames = [] + + function ready() { + frames.push(this.element); + if (frames.length === 2) complete(); + } + + add(HiddenFrame({ onReady: ready })); + add(HiddenFrame({ onReady: ready })); + + function complete() { + frames.forEach(function(frame) { + assert.ok(frame.parentNode, "frame is in the document"); + }) + loader.unload(); + frames.forEach(function(frame) { + assert.ok(!frame.parentNode, "frame isn't in the document'"); + }); + done(); + } +}; + +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 new file mode 100644 index 000000000..1c6664534 --- /dev/null +++ b/addon-sdk/source/test/test-host-events.js @@ -0,0 +1,99 @@ +/* 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, Ci } = require('chrome'); +const { defer, all } = require('sdk/core/promise'); +const { setTimeout } = require('sdk/timers'); +const { request, response } = require('sdk/addon/host'); +const { send } = require('sdk/addon/events'); +const { filter } = require('sdk/event/utils'); +const { on, emit, off } = require('sdk/event/core'); + +var stream = filter(request, (data) => /sdk-x-test/.test(data.event)); + +exports.testSend = function (assert, done) { + on(stream, 'data', handle); + send('sdk-x-test-simple', { title: 'my test data' }).then((data) => { + assert.equal(data.title, 'my response', 'response is handled'); + off(stream, 'data', handle); + done(); + }, (reason) => { + assert.fail('should not call reject'); + }); + function handle (e) { + assert.equal(e.event, 'sdk-x-test-simple', 'correct event name'); + assert.ok(e.id != null, 'message has an ID'); + assert.equal(e.data.title, 'my test data', 'serialized data passes'); + e.data.title = 'my response'; + emit(response, 'data', e); + } +}; + +exports.testSendError = function (assert, done) { + on(stream, 'data', handle); + send('sdk-x-test-error', { title: 'my test data' }).then((data) => { + assert.fail('should not call success'); + }, (reason) => { + assert.equal(reason, 'ErrorInfo', 'should reject with error/reason'); + off(stream, 'data', handle); + done(); + }); + function handle (e) { + e.error = 'ErrorInfo'; + emit(response, 'data', e); + } +}; + +exports.testMultipleSends = function (assert, done) { + let count = 0; + let ids = []; + on(stream, 'data', handle); + ['firefox', 'thunderbird', 'rust'].map(data => + send('sdk-x-test-multi', { data: data }).then(val => { + assert.ok(val === 'firefox' || val === 'rust', 'successful calls resolve correctly'); + if (++count === 3) { + off(stream, 'data', handle); + done(); + } + }, reason => { + assert.equal(reason.error, 'too blue', 'rejected calls are rejected'); + if (++count === 3) { + off(stream, 'data', handle); + done(); + } + })); + + function handle (e) { + if (e.data !== 'firefox' || e.data !== 'rust') + e.error = { data: e.data, error: 'too blue' }; + assert.ok(!~ids.indexOf(e.id), 'ID should be unique'); + assert.equal(e.event, 'sdk-x-test-multi', 'has event name'); + ids.push(e.id); + emit(response, 'data', e); + } +}; + +exports.testSerialization = function (assert, done) { + on(stream, 'data', handle); + let object = { title: 'my test data' }; + let resObject; + send('sdk-x-test-serialize', object).then(data => { + data.title = 'another title'; + assert.equal(object.title, 'my test data', 'original object not modified'); + assert.equal(resObject.title, 'new title', 'object passed by value from host'); + off(stream, 'data', handle); + done(); + }, (reason) => { + assert.fail('should not call reject'); + }); + function handle (e) { + e.data.title = 'new title'; + assert.equal(object.title, 'my test data', 'object passed by value to host'); + resObject = e.data; + emit(response, 'data', e); + } +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-hotkeys.js b/addon-sdk/source/test/test-hotkeys.js new file mode 100644 index 000000000..ba460ee45 --- /dev/null +++ b/addon-sdk/source/test/test-hotkeys.js @@ -0,0 +1,183 @@ +/* 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"); +const { keyDown } = require("sdk/dom/events/keys"); +const { Loader } = require('sdk/test/loader'); +const { getMostRecentBrowserWindow } = require("sdk/window/utils"); + +const element = getMostRecentBrowserWindow().document.documentElement; + +exports["test hotkey: function key"] = function(assert, done) { + var showHotKey = Hotkey({ + combo: "f1", + onPress: function() { + assert.pass("first callback is called"); + assert.equal(this, showHotKey, + 'Context `this` in `onPress` should be the hotkey object'); + keyDown(element, "f2"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "f2", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "f1"); +}; + +exports["test hotkey: accel alt shift"] = function(assert, done) { + var showHotKey = Hotkey({ + combo: "accel-shift-6", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "accel-alt-shift-6"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "accel-alt-shift-6", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "accel-shift-6"); +}; + +exports["test hotkey meta & control"] = function(assert, done) { + var showHotKey = Hotkey({ + combo: "meta-3", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "alt-control-shift-b"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "Ctrl-Alt-Shift-B", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "meta-3"); +}; + +exports["test hotkey: control-1 / meta--"] = function(assert, done) { + var showHotKey = Hotkey({ + combo: "control-1", + onPress: function() { + assert.pass("first callback is called"); + keyDown(element, "meta--"); + showHotKey.destroy(); + } + }); + + var hideHotKey = Hotkey({ + combo: "meta--", + onPress: function() { + assert.pass("second callback is called"); + hideHotKey.destroy(); + done(); + } + }); + + keyDown(element, "control-1"); +}; + +exports["test invalid combos"] = function(assert) { + assert.throws(function() { + Hotkey({ + combo: "d", + onPress: function() {} + }); + }, "throws if no modifier is present"); + assert.throws(function() { + Hotkey({ + combo: "alt", + onPress: function() {} + }); + }, "throws if no key is present"); + assert.throws(function() { + Hotkey({ + combo: "alt p b", + onPress: function() {} + }); + }, "throws if more then one key is present"); +}; + +exports["test no exception on unmodified keypress"] = function(assert) { + var someHotkey = Hotkey({ + combo: "control-alt-1", + onPress: () => {} + }); + keyDown(element, "a"); + assert.pass("No exception throw, unmodified keypress passed"); + someHotkey.destroy(); +}; + +exports["test hotkey: automatic destroy"] = function*(assert) { + // Hacky way to be able to create unloadable modules via makeSandboxedLoader. + let loader = Loader(module); + + var called = false; + var hotkey = loader.require("sdk/hotkeys").Hotkey({ + combo: "accel-shift-x", + onPress: () => 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"); + + assert.ok(!called, "Hotkey is destroyed and not called."); + + // create a new hotkey for a different set + yield new Promise(resolve => { + let key = Hotkey({ + combo: "accel-shift-y", + onPress: () => { + key.destroy(); + assert.pass("accel-shift-y was pressed."); + resolve(); + } + }); + keyDown(element, "accel-shift-y"); + }); + + assert.ok(!called, "Hotkey is still not called, in time it would take."); + + // create a new hotkey for the same set + yield new Promise(resolve => { + let key = Hotkey({ + combo: "accel-shift-x", + onPress: () => { + key.destroy(); + assert.pass("accel-shift-x was pressed."); + resolve(); + } + }); + keyDown(element, "accel-shift-x"); + }); + + assert.ok(!called, "Hotkey is still not called, and reusing is ok."); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-httpd.js b/addon-sdk/source/test/test-httpd.js new file mode 100644 index 000000000..78740f1bf --- /dev/null +++ b/addon-sdk/source/test/test-httpd.js @@ -0,0 +1,73 @@ +/* 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 port = 8099; +const file = require("sdk/io/file"); +const { pathFor } = require("sdk/system"); +const { Loader } = require("sdk/test/loader"); +const options = require("sdk/test/options"); + +const loader = Loader(module); +const httpd = loader.require("./lib/httpd"); +if (options.parseable || options.verbose) + loader.sandbox("./lib/httpd").DEBUG = true; + +exports.testBasicHTTPServer = function(assert, done) { + // Use the profile directory for the temporary file as that will be deleted + // when tests are complete + let basePath = pathFor("ProfD"); + let filePath = file.join(basePath, 'test-httpd.txt'); + let content = "This is the HTTPD test file.\n"; + let fileStream = file.open(filePath, 'w'); + fileStream.write(content); + fileStream.close(); + + let srv = httpd.startServerAsync(port, basePath); + + // Request this very file. + let Request = require('sdk/request').Request; + Request({ + url: "http://localhost:" + port + "/test-httpd.txt", + onComplete: function (response) { + assert.equal(response.text, content); + srv.stop(done); + } + }).get(); +}; + +exports.testDynamicServer = function (assert, done) { + let content = "This is the HTTPD test file.\n"; + + let srv = httpd.startServerAsync(port); + + // See documentation here: + //http://doxygen.db48x.net/mozilla/html/interfacensIHttpServer.html#a81fc7e7e29d82aac5ce7d56d0bedfb3a + //http://doxygen.db48x.net/mozilla/html/interfacensIHttpRequestHandler.html + srv.registerPathHandler("/test-httpd.txt", function handle(request, response) { + // Add text content type, only to avoid error in `Request` API + response.setHeader("Content-Type", "text/plain", false); + response.write(content); + }); + + // Request this very file. + let Request = require('sdk/request').Request; + Request({ + url: "http://localhost:" + port + "/test-httpd.txt", + onComplete: function (response) { + assert.equal(response.text, content); + srv.stop(done); + } + }).get(); +}; + +exports.testAutomaticPortSelection = function (assert, done) { + const srv = httpd.startServerAsync(-1); + + const port = srv.identity.primaryPort; + assert.ok(0 <= port && port <= 65535); + + srv.stop(done); +}; + +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 new file mode 100644 index 000000000..ea53a3e72 --- /dev/null +++ b/addon-sdk/source/test/test-indexed-db.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"; + +const { indexedDB, IDBKeyRange, DOMException + } = require("sdk/indexed-db"); + +exports["test indexedDB is frozen"] = function(assert){ + let original = indexedDB.open; + let f = function(){}; + assert.throws(function(){indexedDB.open = f}); + assert.equal(indexedDB.open,original); + assert.notEqual(indexedDB.open,f); + +}; + +exports["test db variables"] = function(assert) { + [ indexedDB, IDBKeyRange, DOMException + ].forEach(function(value) { + assert.notEqual(typeof(value), "undefined", "variable is defined"); + }); +} + +exports["test open"] = function(assert, done) { + testOpen(0, assert, done); +} + +function testOpen(step, assert, done) { + const dbName = "MyTestDatabase"; + const openParams = [ + { dbName: "MyTestDatabase", dbVersion: 10 }, + { dbName: "MyTestDatabase" }, + { dbName: "MyTestDatabase", dbOptions: { storage: "temporary" } }, + { dbName: "MyTestDatabase", dbOptions: { version: 20, storage: "default" } } + ]; + + let params = openParams[step]; + + let request; + let expectedStorage; + let expectedVersion; + let upgradeNeededCalled = false; + if ("dbVersion" in params) { + request = indexedDB.open(params.dbName, params.dbVersion); + expectedVersion = params.dbVersion; + expectedStorage = "persistent"; + } else if ("dbOptions" in params) { + request = indexedDB.open(params.dbName, params.dbOptions); + if ("version" in params.dbOptions) { + expectedVersion = params.dbOptions.version; + } else { + expectedVersion = 1; + } + if ("storage" in params.dbOptions) { + expectedStorage = params.dbOptions.storage; + } else { + expectedStorage = "persistent"; + } + } else { + request = indexedDB.open(params.dbName); + expectedVersion = 1; + expectedStorage = "persistent"; + } + request.onerror = function(event) { + assert.fail("Failed to open indexedDB") + done(); + } + request.onupgradeneeded = function(event) { + upgradeNeededCalled = true; + assert.equal(event.oldVersion, 0, "Correct old version"); + } + request.onsuccess = function(event) { + assert.pass("IndexedDB was open"); + assert.equal(upgradeNeededCalled, true, "Upgrade needed called"); + let db = request.result; + assert.equal(db.storage, expectedStorage, "Storage is correct"); + db.onversionchange = function(event) { + assert.equal(event.oldVersion, expectedVersion, "Old version is correct"); + db.close(); + } + if ("dbOptions" in params) { + request = indexedDB.deleteDatabase(params.dbName, params.dbOptions); + } else { + request = indexedDB.deleteDatabase(params.dbName); + } + request.onerror = function(event) { + assert.fail("Failed to delete indexedDB") + done(); + } + request.onsuccess = function(event) { + assert.pass("IndexedDB was deleted"); + + if (++step == openParams.length) { + done(); + } else { + testOpen(step, assert, done); + } + } + } +} + +exports["test dbname is unprefixed"] = function(assert, done) { + // verify fixes in https://bugzilla.mozilla.org/show_bug.cgi?id=786688 + let dbName = "dbname-unprefixed"; + let request = indexedDB.open(dbName); + request.onerror = function(event) { + assert.fail("Failed to open db"); + done(); + }; + request.onsuccess = function(event) { + assert.equal(request.result.name, dbName); + done(); + }; +}; + +exports["test structuring the database"] = function(assert, done) { + // This is what our customer data looks like. + let customerData = [ + { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" }, + { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" } + ]; + let dbName = "the_name"; + let request = indexedDB.open(dbName, 2); + request.onerror = function(event) { + assert.fail("Failed to open db"); + done(); + }; + request.onsuccess = function(event) { + assert.pass("transaction is complete"); + testRead(assert, done); + } + request.onupgradeneeded = function(event) { + assert.pass("data base upgrade") + + var db = event.target.result; + + // Create an objectStore to hold information about our customers. We"re + // going to use "ssn" as our key path because it"s guaranteed to be + // unique. + var objectStore = db.createObjectStore("customers", { keyPath: "ssn" }); + + // Create an index to search customers by name. We may have duplicates + // so we can"t use a unique index. + objectStore.createIndex("name", "name", { unique: false }); + + // Create an index to search customers by email. We want to ensure that + // no two customers have the same email, so use a unique index. + objectStore.createIndex("email", "email", { unique: true }); + + // Store values in the newly created objectStore. + customerData.forEach(function(data) { + objectStore.add(data); + }); + assert.pass("data added to object store"); + }; +}; + +function testRead(assert, done) { + let dbName = "the_name"; + let request = indexedDB.open(dbName, 2); + request.onsuccess = function(event) { + assert.pass("data opened") + var db = event.target.result; + let transaction = db.transaction(["customers"]); + var objectStore = transaction.objectStore("customers"); + var request = objectStore.get("444-44-4444"); + request.onerror = function(event) { + assert.fail("Failed to retrive data") + }; + request.onsuccess = function(event) { + // Do something with the request.result! + assert.equal(request.result.name, "Bill", "Name is correct"); + done(); + }; + }; + request.onerror = function() { + assert.fail("failed to open db"); + }; +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-jetpack-id.js b/addon-sdk/source/test/test-jetpack-id.js new file mode 100644 index 000000000..99479e32d --- /dev/null +++ b/addon-sdk/source/test/test-jetpack-id.js @@ -0,0 +1,64 @@ +/* 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 getID = require("jetpack-id/index"); + +exports["test Returns GUID when `id` GUID"] = assert => { + var guid = "{8490ae4f-93bc-13af-80b3-39adf9e7b243}"; + assert.equal(getID({ id: guid }), guid); +}; + +exports["test Returns domain id when `id` domain id"] = assert => { + var id = "my-addon@jetpack"; + assert.equal(getID({ id: id }), id); +}; + +exports["test allows underscores in name"] = assert => { + var name = "my_addon"; + assert.equal(getID({ name: name }), `@${name}`); +}; + +exports["test allows underscores in id"] = assert => { + var id = "my_addon@jetpack"; + assert.equal(getID({ id: id }), id); +}; + +exports["test Returns valid name when `name` exists"] = assert => { + var id = "my-addon"; + assert.equal(getID({ name: id }), `@${id}`); +}; + + +exports["test Returns null when `id` and `name` do not exist"] = assert => { + assert.equal(getID({}), null) +} + +exports["test Returns null when no object passed in"] = assert => { + assert.equal(getID(), null) +} + +exports["test Returns null when `id` exists but not GUID/domain"] = assert => { + var id = "my-addon"; + assert.equal(getID({ id: id }), null); +} + +exports["test Returns null when `id` contains multiple @"] = assert => { + assert.equal(getID({ id: "my@addon@yeah" }), null); +}; + +exports["test Returns null when `id` or `name` specified in domain format but has invalid characters"] = assert => { + [" ", "!", "/", "$", " ", "~", "("].forEach(sym => { + assert.equal(getID({ id: "my" + sym + "addon@domain" }), null); + assert.equal(getID({ name: "my" + sym + "addon" }), null); + }); +}; + +exports["test Returns null, does not crash, when providing non-string properties for `name` and `id`"] = assert => { + assert.equal(getID({ id: 5 }), null); + assert.equal(getID({ name: 5 }), null); + assert.equal(getID({ name: {} }), null); +}; + +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 new file mode 100644 index 000000000..18f32eab3 --- /dev/null +++ b/addon-sdk/source/test/test-keyboard-observer.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"; + +const { keyPress } = require("sdk/dom/events/keys"); +const { Loader } = require("sdk/test/loader"); +const timer = require("sdk/timers"); + +exports["test unload keyboard observer"] = function(assert, done) { + let loader = Loader(module); + let element = loader.require("sdk/deprecated/window-utils"). + activeBrowserWindow.document.documentElement; + let observer = loader.require("sdk/keyboard/observer"). + observer; + let called = 0; + + observer.on("keypress", function () { called++; }); + + // dispatching "keypress" event to trigger observer listeners. + keyPress(element, "accel-%"); + + // Unload the module. + loader.unload(); + + // dispatching "keypress" even once again. + keyPress(element, "accel-%"); + + // Enqueuing asserts to make sure that assertion is not performed early. + timer.setTimeout(function () { + assert.equal(called, 1, "observer was called before unload only."); + done(); + }, 0); +}; + +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 new file mode 100644 index 000000000..00fc841ee --- /dev/null +++ b/addon-sdk/source/test/test-keyboard-utils.js @@ -0,0 +1,61 @@ +/* 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"); +const runtime = require("sdk/system/runtime"); + +const isMac = runtime.OS === "Darwin"; + +exports["test toString"] = function(assert) { + assert.equal(utils.toString({ + key: "B", + modifiers: [ "Shift", "Ctrl" ] + }), "Shift-Ctrl-B", "toString does not normalizes JSON"); + + assert.equal(utils.toString({ + key: "C", + modifiers: [], + }), "C", "Works with objects with empty array of modifiers"); + + assert.equal(utils.toString(Object.create((function Type() {}).prototype, { + key: { value: "d" }, + modifiers: { value: [ "alt" ] }, + method: { value: function() {} } + })), "alt-d", "Works with non-json objects"); + + assert.equal(utils.toString({ + modifiers: [ "shift", "alt" ] + }), "shift-alt-", "works with only modifiers"); +}; + +exports["test toJSON"] = function(assert) { + assert.deepEqual(utils.toJSON("Shift-Ctrl-B"), { + key: "b", + modifiers: [ "control", "shift" ] + }, "toJSON normalizes input"); + + assert.deepEqual(utils.toJSON("Meta-Alt-option-C"), { + key: "c", + modifiers: [ "alt", "meta" ] + }, "removes dublicates"); + + assert.deepEqual(utils.toJSON("AccEl+sHiFt+Z", "+"), { + key: "z", + modifiers: isMac ? [ "meta", "shift" ] : [ "control", "shift" ] + }, "normalizes OS specific keys and adjustes seperator"); +}; + +exports["test normalize"] = function assert(assert) { + assert.equal(utils.normalize("Shift Ctrl A control ctrl", " "), + "control shift a", "removes reapeted modifiers"); + assert.equal(utils.normalize("shift-ctrl-left"), "control-shift-left", + "normilizes non printed characters"); + + assert.throws(function() { + utils.normalize("shift-alt-b-z"); + }, "throws if contains more then on non-modifier key"); +}; + +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 new file mode 100644 index 000000000..564abbf1b --- /dev/null +++ b/addon-sdk/source/test/test-l10n-locale.js @@ -0,0 +1,169 @@ +/* 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 { getPreferedLocales, findClosestLocale } = require("sdk/l10n/locale"); +const prefs = require("sdk/preferences/service"); +const { Cc, Ci, Cu } = require("chrome"); +const { Services } = Cu.import("resource://gre/modules/Services.jsm"); +const BundleService = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService); + +const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; +const PREF_SELECTED_LOCALE = "general.useragent.locale"; +const PREF_ACCEPT_LANGUAGES = "intl.accept_languages"; + +function assertPrefered(assert, expected, msg) { + assert.equal(JSON.stringify(getPreferedLocales()), JSON.stringify(expected), + msg); +} + +exports.testGetPreferedLocales = function(assert) { + prefs.set(PREF_MATCH_OS_LOCALE, false); + prefs.set(PREF_SELECTED_LOCALE, ""); + prefs.set(PREF_ACCEPT_LANGUAGES, ""); + assertPrefered(assert, ["en-us"], + "When all preferences are empty, we only have en-us"); + + prefs.set(PREF_SELECTED_LOCALE, "fr"); + prefs.set(PREF_ACCEPT_LANGUAGES, "jp"); + assertPrefered(assert, ["fr", "jp", "en-us"], + "We first have useragent locale, then web one and finally en-US"); + + prefs.set(PREF_SELECTED_LOCALE, "en-US"); + prefs.set(PREF_ACCEPT_LANGUAGES, "en-US"); + assertPrefered(assert, ["en-us"], + "We do not have duplicates"); + + prefs.set(PREF_SELECTED_LOCALE, "en-US"); + prefs.set(PREF_ACCEPT_LANGUAGES, "fr"); + assertPrefered(assert, ["en-us", "fr"], + "en-US can be first if specified by higher priority preference"); + + // Reset what we changed + prefs.reset(PREF_MATCH_OS_LOCALE); + prefs.reset(PREF_SELECTED_LOCALE); + prefs.reset(PREF_ACCEPT_LANGUAGES); +} + +// In some cases, mainly on Fennec and on Linux version, +// `general.useragent.locale` is a special 'localized' value, like: +// "chrome://global/locale/intl.properties" +exports.testPreferedLocalizedLocale = function(assert) { + prefs.set(PREF_MATCH_OS_LOCALE, false); + let bundleURL = "chrome://global/locale/intl.properties"; + prefs.setLocalized(PREF_SELECTED_LOCALE, bundleURL); + let contentLocale = "ja"; + prefs.set(PREF_ACCEPT_LANGUAGES, contentLocale); + + // Read manually the expected locale value from the property file + let expectedLocale = BundleService.createBundle(bundleURL). + GetStringFromName(PREF_SELECTED_LOCALE). + toLowerCase(); + + // First add the useragent locale + let expectedLocaleList = [expectedLocale]; + + // Then the content locale + if (expectedLocaleList.indexOf(contentLocale) == -1) + expectedLocaleList.push(contentLocale); + + // 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 pref value"); + + // Reset what we have changed + prefs.reset(PREF_MATCH_OS_LOCALE); + prefs.reset(PREF_SELECTED_LOCALE); + 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, ""); + prefs.set(PREF_ACCEPT_LANGUAGES, ""); + + let expectedLocale = Services.locale.getLocaleComponentForUserAgent(). + toLowerCase(); + let expectedLocaleList = [expectedLocale]; + + // Add default "en-us" fallback if the main language is not already en-us + if (expectedLocale != "en-us") + expectedLocaleList.push("en-us"); + + assertPrefered(assert, expectedLocaleList, "Ensure that we select OS locale when related preference is set"); + + // Reset what we have changed + prefs.reset(PREF_MATCH_OS_LOCALE); + prefs.reset(PREF_SELECTED_LOCALE); + prefs.reset(PREF_ACCEPT_LANGUAGES); +} + +exports.testFindClosestLocale = function(assert) { + // Second param of findClosestLocale (aMatchLocales) have to be in lowercase + assert.equal(findClosestLocale([], []), null, + "When everything is empty we get null"); + + assert.equal(findClosestLocale(["en", "en-US"], ["en"]), + "en", "We always accept exact match first 1/5"); + assert.equal(findClosestLocale(["en-US", "en"], ["en"]), + "en", "We always accept exact match first 2/5"); + assert.equal(findClosestLocale(["en", "en-US"], ["en-us"]), + "en-US", "We always accept exact match first 3/5"); + assert.equal(findClosestLocale(["ja-JP-mac", "ja", "ja-JP"], ["ja-jp"]), + "ja-JP", "We always accept exact match first 4/5"); + assert.equal(findClosestLocale(["ja-JP-mac", "ja", "ja-JP"], ["ja-jp-mac"]), + "ja-JP-mac", "We always accept exact match first 5/5"); + + assert.equal(findClosestLocale(["en", "en-GB"], ["en-us"]), + "en", "We accept more generic locale, when there is no exact match 1/2"); + assert.equal(findClosestLocale(["en-ZA", "en"], ["en-gb"]), + "en", "We accept more generic locale, when there is no exact match 2/2"); + + assert.equal(findClosestLocale(["ja-JP"], ["ja"]), + "ja-JP", "We accept more specialized locale, when there is no exact match 1/2"); + // Better to select "ja" in this case but behave same as current AddonManager + assert.equal(findClosestLocale(["ja-JP-mac", "ja"], ["ja-jp"]), + "ja-JP-mac", "We accept more specialized locale, when there is no exact match 2/2"); + + assert.equal(findClosestLocale(["en-US"], ["en-us"]), + "en-US", "We keep the original one as result 1/2"); + assert.equal(findClosestLocale(["en-us"], ["en-us"]), + "en-us", "We keep the original one as result 2/2"); + + assert.equal(findClosestLocale(["ja-JP-mac"], ["ja-jp-mac"]), + "ja-JP-mac", "We accept locale with 3 parts"); + assert.equal(findClosestLocale(["ja-JP"], ["ja-jp-mac"]), + "ja-JP", "We accept locale with 2 parts from locale with 3 parts"); + assert.equal(findClosestLocale(["ja"], ["ja-jp-mac"]), + "ja", "We accept locale with 1 part from locale with 3 parts"); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-l10n-plural-rules.js b/addon-sdk/source/test/test-l10n-plural-rules.js new file mode 100644 index 000000000..953d977a4 --- /dev/null +++ b/addon-sdk/source/test/test-l10n-plural-rules.js @@ -0,0 +1,85 @@ +/* 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 { getRulesForLocale } = require("sdk/l10n/plural-rules"); + +// For more information, please visit unicode website: +// http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html + +function map(assert, f, n, form) { + assert.equal(f(n), form, n + " maps to '" + form + "'"); +} + +exports.testFrench = function(assert) { + let f = getRulesForLocale("fr"); + map(assert, f, -1, "other"); + map(assert, f, 0, "one"); + map(assert, f, 1, "one"); + map(assert, f, 1.5, "one"); + map(assert, f, 2, "other"); + map(assert, f, 100, "other"); +} + +exports.testEnglish = function(assert) { + let f = getRulesForLocale("en"); + map(assert, f, -1, "other"); + map(assert, f, 0, "other"); + map(assert, f, 1, "one"); + map(assert, f, 1.5, "other"); + map(assert, f, 2, "other"); + map(assert, f, 100, "other"); +} + +exports.testArabic = function(assert) { + let f = getRulesForLocale("ar"); + map(assert, f, -1, "other"); + map(assert, f, 0, "zero"); + map(assert, f, 0.5, "other"); + + map(assert, f, 1, "one"); + map(assert, f, 1.5, "other"); + + map(assert, f, 2, "two"); + map(assert, f, 2.5, "other"); + + map(assert, f, 3, "few"); + map(assert, f, 3.5, "few"); // I'd expect it to be 'other', but the unicode.org + // algorithm computes 'few'. + map(assert, f, 5, "few"); + map(assert, f, 10, "few"); + map(assert, f, 103, "few"); + map(assert, f, 105, "few"); + map(assert, f, 110, "few"); + map(assert, f, 203, "few"); + map(assert, f, 205, "few"); + map(assert, f, 210, "few"); + + map(assert, f, 11, "many"); + map(assert, f, 50, "many"); + map(assert, f, 99, "many"); + map(assert, f, 111, "many"); + map(assert, f, 150, "many"); + map(assert, f, 199, "many"); + + map(assert, f, 100, "other"); + map(assert, f, 101, "other"); + map(assert, f, 102, "other"); + map(assert, f, 200, "other"); + map(assert, f, 201, "other"); + map(assert, f, 202, "other"); +} + +exports.testJapanese = function(assert) { + // Japanese doesn't have plural forms. + let f = getRulesForLocale("ja"); + map(assert, f, -1, "other"); + map(assert, f, 0, "other"); + map(assert, f, 1, "other"); + map(assert, f, 1.5, "other"); + map(assert, f, 2, "other"); + map(assert, f, 100, "other"); +} + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-lang-type.js b/addon-sdk/source/test/test-lang-type.js new file mode 100644 index 000000000..c0e510076 --- /dev/null +++ b/addon-sdk/source/test/test-lang-type.js @@ -0,0 +1,166 @@ +/* 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.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.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.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 array"] = function (assert) { + assert.equal(utils.isArray([]), true, "[] is an array"); + assert.equal(utils.isArray([1]), true, "[1] is an array"); + assert.equal(utils.isArray(new Array()), true, "new Array() is an array"); + assert.equal(utils.isArray(new Array(10)), true, "new Array(10) is an array"); + assert.equal(utils.isArray(Array.prototype), true, "Array.prototype is an array"); + + assert.equal(utils.isArray(), false, "implicit undefined is not an array"); + assert.equal(utils.isArray(null), false, "null is not an array"); + assert.equal(utils.isArray(undefined), false, "undefined is not an array"); + assert.equal(utils.isArray(1), false, "1 is not an array"); + assert.equal(utils.isArray(true), false, "true is not an array"); + assert.equal(utils.isArray('foo'), false, "'foo' is not an array"); + assert.equal(utils.isArray({}), false, "{} is not an array"); + assert.equal(utils.isArray(Symbol.iterator), false, "Symbol.iterator is not an array"); +}; + +exports["test arguments"] = function (assert) { + assert.equal(utils.isArguments(arguments), true, "arguments is an arguments"); + (function() { + assert.equal(utils.isArguments(arguments), true, "arguments in nested function is an arguments"); + })(); + (function*() { + assert.equal(utils.isArguments(arguments), true, "arguments in nested generator is an arguments"); + })(); + (() => { + assert.equal(utils.isArguments(arguments), true, "arguments in arrow function is an arguments"); + })(); + + assert.equal(utils.isArguments(), false, "implicit undefined is not an arguments"); + assert.equal(utils.isArguments(null), false, "null is not an arguments"); + assert.equal(utils.isArguments(undefined), false, "undefined is not an arguments"); + assert.equal(utils.isArguments(1), false, "1 is not an arguments"); + assert.equal(utils.isArguments(true), false, "true is not an arguments"); + assert.equal(utils.isArguments('foo'), false, "'foo' is not an arguments"); + assert.equal(utils.isArguments([]), false, "[] is not an arguments"); + assert.equal(utils.isArguments({}), false, "{} is not an arguments"); + assert.equal(utils.isArguments(Symbol.iterator), false, "Symbol.iterator is not an arguments"); + (function(...args) { + assert.equal(utils.isArguments(args), false, "rest arguments is not an arguments"); + })(); +}; + +exports["test flat objects"] = function (assert) { + assert.ok(utils.isFlat({}), "`{}` is a flat object"); + assert.ok(!utils.isFlat([]), "`[]` is not a flat object"); + assert.ok(!utils.isFlat(new function() {}), "derived objects are not flat"); + assert.ok(utils.isFlat(Object.prototype), "Object.prototype is flat"); +}; + +exports["test json atoms"] = function (assert) { + assert.ok(utils.isJSON(null), "`null` is JSON"); + assert.ok(utils.isJSON(undefined), "`undefined` is JSON"); + assert.ok(utils.isJSON(NaN), "`NaN` is JSON"); + assert.ok(utils.isJSON(Infinity), "`Infinity` is JSON"); + assert.ok(utils.isJSON(true) && utils.isJSON(false), "booleans are JSON"); + assert.ok(utils.isJSON(4), utils.isJSON(0), "numbers are JSON"); + assert.ok(utils.isJSON("foo bar"), "strings are JSON"); +}; + +exports["test jsonable values"] = function (assert) { + assert.ok(utils.isJSONable(null), "`null` is JSONable"); + assert.ok(!utils.isJSONable(undefined), "`undefined` is not JSONable"); + assert.ok(utils.isJSONable(NaN), "`NaN` is JSONable"); + assert.ok(utils.isJSONable(Infinity), "`Infinity` is JSONable"); + assert.ok(utils.isJSONable(true) && utils.isJSONable(false), "booleans are JSONable"); + assert.ok(utils.isJSONable(0), "numbers are JSONable"); + assert.ok(utils.isJSONable("foo bar"), "strings are JSONable"); + assert.ok(!utils.isJSONable(function(){}), "functions are not JSONable"); + + const functionWithToJSON = function(){}; + functionWithToJSON.toJSON = function() { return "foo bar"; }; + assert.ok(utils.isJSONable(functionWithToJSON), "functions with toJSON() are JSONable"); + + assert.ok(utils.isJSONable({}), "`{}` is JSONable"); + + const foo = {}; + foo.bar = foo; + assert.ok(!utils.isJSONable(foo), "recursive objects are not JSONable"); +}; + +exports["test instanceOf"] = function (assert) { + assert.ok(utils.instanceOf(assert, Object), + "assert is object from other sandbox"); + assert.ok(utils.instanceOf(new Date(), Date), "instance of date"); + assert.ok(!utils.instanceOf(null, Object), "null is not an instance"); +}; + +exports["test json"] = function (assert) { + assert.ok(!utils.isJSON(function(){}), "functions are not json"); + assert.ok(utils.isJSON({}), "`{}` is JSON"); + assert.ok(utils.isJSON({ + a: "foo", + b: 3, + c: undefined, + d: null, + e: { + f: { + g: "bar", + p: [{}, "oueou", 56] + }, + q: { nan: NaN, infinity: Infinity }, + "non standard name": "still works" + } + }), "JSON can contain nested objects"); + + var foo = {}; + var bar = { foo: foo }; + foo.bar = bar; + assert.ok(!utils.isJSON(foo), "recursive objects are not json"); + + + assert.ok(!utils.isJSON({ get foo() { return 5 } }), + "json can not have getter"); + + assert.ok(!utils.isJSON({ foo: "bar", baz: function () {} }), + "json can not contain functions"); + + assert.ok(!utils.isJSON(Object.create({})), + "json must be direct descendant of `Object.prototype`"); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-libxul.js b/addon-sdk/source/test/test-libxul.js new file mode 100644 index 000000000..7a11a69cb --- /dev/null +++ b/addon-sdk/source/test/test-libxul.js @@ -0,0 +1,18 @@ +/* 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 that we can link with libxul using js-ctypes + +const {Cu} = require("chrome"); +const {ctypes} = Cu.import("resource://gre/modules/ctypes.jsm", {}); +const {OS} = Cu.import("resource://gre/modules/osfile.jsm", {}); + +exports.test = function(assert) { + let path = OS.Constants.Path.libxul; + assert.pass("libxul is at " + path); + let lib = ctypes.open(path); + assert.ok(lib != null, "linked to libxul successfully"); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-list.js b/addon-sdk/source/test/test-list.js new file mode 100644 index 000000000..9b03a9513 --- /dev/null +++ b/addon-sdk/source/test/test-list.js @@ -0,0 +1,58 @@ +/* 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 { List, addListItem, removeListItem } = require('sdk/util/list'); +const { Class } = require('sdk/core/heritage'); + +exports.testList = function(assert) { + let list = List(); + addListItem(list, 1); + + for (let key in list) { + assert.equal(key, 0, 'key is correct'); + assert.equal(list[key], 1, 'value is correct'); + } + + let count = 0; + for (let ele of list) { + assert.equal(ele, 1, 'ele is correct'); + assert.equal(++count, 1, 'count is correct'); + } + + count = 0; + for (let ele of list) { + assert.equal(ele, 1, 'ele is correct'); + assert.equal(++count, 1, 'count is correct'); + } + + removeListItem(list, 1); + assert.equal(list.length, 0, 'remove worked'); +}; + +exports.testImplementsList = function(assert) { + let List2 = Class({ + implements: [List], + initialize: function() { + List.prototype.initialize.apply(this, [0, 1, 2]); + } + }); + let list2 = List2(); + let count = 0; + + for (let ele of list2) { + assert.equal(ele, count++, 'ele is correct'); + } + + count = 0; + for (let ele of list2) { + assert.equal(ele, count++, 'ele is correct'); + } + + addListItem(list2, 3); + assert.equal(list2.length, 4, '3 was added'); + assert.equal(list2[list2.length-1], 3, '3 was added'); +} + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-loader.js b/addon-sdk/source/test/test-loader.js new file mode 100644 index 000000000..3ee3e34f0 --- /dev/null +++ b/addon-sdk/source/test/test-loader.js @@ -0,0 +1,657 @@ +/* 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 { + Loader, main, unload, parseStack, resolve, join, + Require, Module +} = require('toolkit/loader'); +var { readURI } = require('sdk/net/url'); + +var 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'); +const { addDebuggerToGlobal } = Cu.import('resource://gre/modules/jsdebugger.jsm', {}); +addDebuggerToGlobal(this); + +exports['test resolve'] = function (assert) { + let cuddlefish_id = 'sdk/loader/cuddlefish'; + assert.equal(resolve('../index.js', './dir/c.js'), './index.js'); + assert.equal(resolve('./index.js', './dir/c.js'), './dir/index.js'); + assert.equal(resolve('./dir/c.js', './index.js'), './dir/c.js'); + assert.equal(resolve('../utils/file.js', './dir/b.js'), './utils/file.js'); + + assert.equal(resolve('../utils/./file.js', './dir/b.js'), './utils/file.js'); + assert.equal(resolve('../utils/file.js', './'), './../utils/file.js'); + assert.equal(resolve('./utils/file.js', './'), './utils/file.js'); + assert.equal(resolve('./utils/file.js', './index.js'), './utils/file.js'); + + assert.equal(resolve('../utils/./file.js', cuddlefish_id), 'sdk/utils/file.js'); + assert.equal(resolve('../utils/file.js', cuddlefish_id), 'sdk/utils/file.js'); + assert.equal(resolve('./utils/file.js', cuddlefish_id), 'sdk/loader/utils/file.js'); + + assert.equal(resolve('..//index.js', './dir/c.js'), './index.js'); + assert.equal(resolve('../modules/XPCOMUtils.jsm', 'resource://gre/utils/file.js'), 'resource://gre/modules/XPCOMUtils.jsm'); + assert.equal(resolve('../modules/XPCOMUtils.jsm', 'chrome://gre/utils/file.js'), 'chrome://gre/modules/XPCOMUtils.jsm'); + assert.equal(resolve('../../a/b/c.json', 'file:///thing/utils/file.js'), 'file:///a/b/c.json'); + + // Does not change absolute paths + assert.equal(resolve('resource://gre/modules/file.js', './dir/b.js'), + 'resource://gre/modules/file.js'); + assert.equal(resolve('file:///gre/modules/file.js', './dir/b.js'), + 'file:///gre/modules/file.js'); + assert.equal(resolve('/root.js', './dir/b.js'), + '/root.js'); +}; + +exports['test join'] = function (assert) { + assert.equal(join('a/path', '../../../module'), '../module'); + assert.equal(join('a/path/to', '../module'), 'a/path/module'); + assert.equal(join('a/path/to', './module'), 'a/path/to/module'); + assert.equal(join('a/path/to', '././../module'), 'a/path/module'); + assert.equal(join('resource://my/path/yeah/yuh', '../whoa'), + 'resource://my/path/yeah/whoa'); + assert.equal(join('resource://my/path/yeah/yuh', './whoa'), + 'resource://my/path/yeah/yuh/whoa'); + assert.equal(join('resource:///my/path/yeah/yuh', '../whoa'), + 'resource:///my/path/yeah/whoa'); + assert.equal(join('resource:///my/path/yeah/yuh', './whoa'), + 'resource:///my/path/yeah/yuh/whoa'); + assert.equal(join('file:///my/path/yeah/yuh', '../whoa'), + 'file:///my/path/yeah/whoa'); + assert.equal(join('file:///my/path/yeah/yuh', './whoa'), + 'file:///my/path/yeah/yuh/whoa'); + assert.equal(join('a/path/to', '..//module'), 'a/path/module'); +}; + +exports['test dependency cycles'] = function(assert) { + let uri = root + '/fixtures/loader/cycles/'; + let loader = Loader({ paths: { '': uri } }); + + let program = main(loader, 'main'); + + assert.equal(program.a.b, program.b, 'module `a` gets correct `b`'); + assert.equal(program.b.a, program.a, 'module `b` gets correct `a`'); + assert.equal(program.c.main, program, 'module `c` gets correct `main`'); + + unload(loader); +} + +exports['test syntax errors'] = function(assert) { + let uri = root + '/fixtures/loader/syntax-error/'; + let loader = Loader({ paths: { '': uri } }); + + try { + let program = main(loader, 'main'); + } catch (error) { + assert.equal(error.name, "SyntaxError", "throws syntax error"); + assert.equal(error.fileName.split("/").pop(), "error.js", + "Error contains filename"); + assert.equal(error.lineNumber, 11, "error is on line 11"); + let stack = parseStack(error.stack); + + assert.equal(stack.pop().fileName, uri + "error.js", + "last frame file containing syntax error"); + assert.equal(stack.pop().fileName, uri + "main.js", + "previous frame is a requirer module"); + assert.equal(stack.pop().fileName, module.uri, + "previous to it is a test module"); + + } finally { + unload(loader); + } +} + +exports['test sandboxes are not added if error'] = function (assert) { + let uri = root + '/fixtures/loader/missing-twice/'; + let loader = Loader({ paths: { '': uri } }); + let program = main(loader, 'main'); + assert.ok(!(uri + 'not-found.js' in loader.sandboxes), 'not-found.js not in loader.sandboxes'); +} + +exports['test missing module'] = function(assert) { + let uri = root + '/fixtures/loader/missing/' + let loader = Loader({ paths: { '': uri } }); + + try { + let program = main(loader, 'main') + } catch (error) { + assert.equal(error.message, "Module `not-found` is not found at " + + uri + "not-found.js", "throws if error not found"); + + assert.equal(error.fileName.split("/").pop(), "main.js", + "Error fileName is requirer module"); + + assert.equal(error.lineNumber, 7, "error is on line 7"); + + let stack = parseStack(error.stack); + + assert.equal(stack.pop().fileName, uri + "main.js", + "loader stack is omitted"); + + assert.equal(stack.pop().fileName, module.uri, + "previous in the stack is test module"); + } finally { + unload(loader); + } +} + +exports["test invalid module not cached and throws everytime"] = function(assert) { + let uri = root + "/fixtures/loader/missing-twice/"; + let loader = Loader({ paths: { "": uri } }); + + let { firstError, secondError, invalidJSON1, invalidJSON2 } = main(loader, "main"); + assert.equal(firstError.message, "Module `not-found` is not found at " + + uri + "not-found.js", "throws on first invalid require"); + assert.equal(firstError.lineNumber, 8, "first error is on line 7"); + assert.equal(secondError.message, "Module `not-found` is not found at " + + uri + "not-found.js", "throws on second invalid require"); + assert.equal(secondError.lineNumber, 14, "second error is on line 14"); + + assert.equal(invalidJSON1.message, + "JSON.parse: unexpected character at line 1 column 1 of the JSON data", + "throws on invalid JSON"); + assert.equal(invalidJSON2.message, + "JSON.parse: unexpected character at line 1 column 1 of the JSON data", + "throws on invalid JSON second time"); +}; + +exports['test exceptions in modules'] = function(assert) { + let uri = root + '/fixtures/loader/exceptions/' + + let loader = Loader({ paths: { '': uri } }); + + try { + let program = main(loader, 'main') + } catch (error) { + assert.equal(error.message, "Boom!", "thrown errors propagate"); + + assert.equal(error.fileName.split("/").pop(), "boomer.js", + "Error comes from the module that threw it"); + + assert.equal(error.lineNumber, 8, "error is on line 8"); + + let stack = parseStack(error.stack); + + let frame = stack.pop() + assert.equal(frame.fileName, uri + "boomer.js", + "module that threw is first in the stack"); + assert.equal(frame.name, "exports.boom", + "name is in the stack"); + + frame = stack.pop() + assert.equal(frame.fileName, uri + "main.js", + "module that called it is next in the stack"); + assert.equal(frame.lineNumber, 9, "caller line is in the stack"); + + + assert.equal(stack.pop().fileName, module.uri, + "this test module is next in the stack"); + } finally { + unload(loader); + } +} + +exports['test early errors in module'] = function(assert) { + let uri = root + '/fixtures/loader/errors/'; + let loader = Loader({ paths: { '': uri } }); + + try { + let program = main(loader, 'main') + } catch (error) { + assert.equal(String(error), + "Error: opening input stream (invalid filename?)", + "thrown errors propagate"); + + assert.equal(error.fileName.split("/").pop(), "boomer.js", + "Error comes from the module that threw it"); + + assert.equal(error.lineNumber, 7, "error is on line 7"); + + let stack = parseStack(error.stack); + + let frame = stack.pop() + assert.equal(frame.fileName, uri + "boomer.js", + "module that threw is first in the stack"); + + frame = stack.pop() + assert.equal(frame.fileName, uri + "main.js", + "module that called it is next in the stack"); + assert.equal(frame.lineNumber, 7, "caller line is in the stack"); + + + assert.equal(stack.pop().fileName, module.uri, + "this test module is next in the stack"); + } finally { + unload(loader); + } +}; + +exports['test require json'] = function (assert) { + let data = require('./fixtures/loader/json/manifest.json'); + assert.equal(data.name, 'Jetpack Loader Test', 'loads json with strings'); + assert.equal(data.version, '1.0.1', 'loads json with strings'); + assert.equal(data.dependencies.async, '*', 'loads json with objects'); + assert.equal(data.dependencies.underscore, '*', 'loads json with objects'); + assert.equal(data.contributors.length, 4, 'loads json with arrays'); + assert.ok(Array.isArray(data.contributors), 'loads json with arrays'); + data.version = '2.0.0'; + let newdata = require('./fixtures/loader/json/manifest.json'); + assert.equal(newdata.version, '2.0.0', + 'JSON objects returned should be cached and the same instance'); + + try { + require('./fixtures/loader/json/invalid.json'); + assert.fail('Error not thrown when loading invalid json'); + } catch (err) { + assert.ok(err, 'error thrown when loading invalid json'); + assert.ok(/JSON\.parse/.test(err.message), + 'should thrown an error from JSON.parse, not attempt to load .json.js'); + } + + // Try again to ensure an empty module isn't loaded from cache + try { + require('./fixtures/loader/json/invalid.json'); + assert.fail('Error not thrown when loading invalid json a second time'); + } catch (err) { + assert.ok(err, + 'error thrown when loading invalid json a second time'); + assert.ok(/JSON\.parse/.test(err.message), + 'should thrown an error from JSON.parse a second time, not attempt to load .json.js'); + } +}; + +exports['test setting metadata for newly created sandboxes'] = function(assert) { + let addonID = 'random-addon-id'; + let uri = root + '/fixtures/loader/cycles/'; + let loader = Loader({ paths: { '': uri }, id: addonID }); + + let dbg = new Debugger(); + dbg.onNewGlobalObject = function(global) { + dbg.onNewGlobalObject = undefined; + + let metadata = Cu.getSandboxMetadata(global.unsafeDereference()); + assert.ok(metadata, 'this global has attached metadata'); + assert.equal(metadata.URI, uri + 'main.js', 'URI is set properly'); + assert.equal(metadata.addonID, addonID, 'addon ID is set'); + } + + let program = main(loader, 'main'); +}; + +exports['test require .json, .json.js'] = function (assert) { + let testjson = require('./fixtures/loader/json/test.json'); + assert.equal(testjson.filename, 'test.json', + 'require("./x.json") should load x.json, not x.json.js'); + + let nodotjson = require('./fixtures/loader/json/nodotjson.json'); + assert.equal(nodotjson.filename, 'nodotjson.json.js', + 'require("./x.json") should load x.json.js when x.json does not exist'); + nodotjson.data.prop = 'hydralisk'; + + // require('nodotjson.json') and require('nodotjson.json.js') + // should resolve to the same file + let nodotjsonjs = require('./fixtures/loader/json/nodotjson.json.js'); + assert.equal(nodotjsonjs.data.prop, 'hydralisk', + 'js modules are cached whether access via .json.js or .json'); +}; + +exports['test invisibleToDebugger: false'] = function (assert) { + let uri = root + '/fixtures/loader/cycles/'; + let loader = Loader({ paths: { '': uri } }); + main(loader, 'main'); + + let dbg = new Debugger(); + let sandbox = loader.sandboxes[uri + 'main.js']; + + try { + dbg.addDebuggee(sandbox); + assert.ok(true, 'debugger added visible value'); + } catch(e) { + assert.fail('debugger could not add visible value'); + } +}; + +exports['test invisibleToDebugger: true'] = function (assert) { + let uri = root + '/fixtures/loader/cycles/'; + let loader = Loader({ paths: { '': uri }, invisibleToDebugger: true }); + main(loader, 'main'); + + let dbg = new Debugger(); + let sandbox = loader.sandboxes[uri + 'main.js']; + + try { + dbg.addDebuggee(sandbox); + assert.fail('debugger added invisible value'); + } catch(e) { + assert.ok(true, 'debugger did not add invisible value'); + } +}; + +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'); + + let loader2 = Loader({ paths: { '': uri }, globals: { console: fakeConsole }}); + let program2 = main(loader2, 'main'); + + assert.equal(program2.console, fakeConsole, + 'global console can be overridden with Loader options'); + function fakeConsole () {}; +}; + +exports['test shared globals'] = function(assert) { + let uri = root + '/fixtures/loader/cycles/'; + let loader = Loader({ paths: { '': uri }, sharedGlobal: true, + sharedGlobalBlocklist: ['b'] }); + + let program = main(loader, 'main'); + + // As it is hard to verify what is the global of an object + // (due to wrappers) we check that we see the `foo` symbol + // being manually injected into the shared global object + loader.sharedGlobalSandbox.foo = true; + + let m = loader.sandboxes[uri + 'main.js']; + let a = loader.sandboxes[uri + 'a.js']; + let b = loader.sandboxes[uri + 'b.js']; + + assert.ok(Cu.getGlobalForObject(m).foo, "main is shared"); + assert.ok(Cu.getGlobalForObject(a).foo, "a is shared"); + assert.ok(!Cu.getGlobalForObject(b).foo, "b isn't shared"); + + unload(loader); +} + +exports['test deprecated shared globals exception name'] = function(assert) { + let uri = root + '/fixtures/loader/cycles/'; + let loader = Loader({ paths: { '': uri }, sharedGlobal: true, + sharedGlobalBlacklist: ['b'] }); + + let program = main(loader, 'main'); + + assert.ok(loader.sharedGlobalBlocklist.includes("b"), "b should be in the blocklist"); + assert.equal(loader.sharedGlobalBlocklist.length, loader.sharedGlobalBlacklist.length, + "both blocklists should have the same number of items."); + assert.equal(loader.sharedGlobalBlocklist.join(","), loader.sharedGlobalBlacklist.join(","), + "both blocklists should have the same items."); + + // As it is hard to verify what is the global of an object + // (due to wrappers) we check that we see the `foo` symbol + // being manually injected into the shared global object + loader.sharedGlobalSandbox.foo = true; + + let m = loader.sandboxes[uri + 'main.js']; + let a = loader.sandboxes[uri + 'a.js']; + let b = loader.sandboxes[uri + 'b.js']; + + assert.ok(Cu.getGlobalForObject(m).foo, "main is shared"); + assert.ok(Cu.getGlobalForObject(a).foo, "a is shared"); + assert.ok(!Cu.getGlobalForObject(b).foo, "b isn't shared"); + + unload(loader); +} + +exports['test prototype of global'] = function (assert) { + let uri = root + '/fixtures/loader/globals/'; + let loader = Loader({ paths: { '': uri }, sharedGlobal: true, + sandboxPrototype: { globalFoo: 5 }}); + + let program = main(loader, 'main'); + + assert.ok(program.globalFoo === 5, '`globalFoo` exists'); +}; + +exports["test require#resolve"] = function(assert) { + let foundRoot = require.resolve("sdk/tabs").replace(/sdk\/tabs.js$/, ""); + assert.ok(root, foundRoot, "correct resolution root"); + + assert.equal(foundRoot + "sdk/tabs.js", require.resolve("sdk/tabs"), "correct resolution of sdk module"); + assert.equal(foundRoot + "toolkit/loader.js", require.resolve("toolkit/loader"), "correct resolution of sdk module"); + + const localLoader = Loader({ + paths: { "foo/bar": "bizzle", + "foo/bar2/": "bizzle2", + // Just to make sure this doesn't match the first entry, + // let use resolve this module + "foo/bar-bar": "foo/bar-bar" } + }); + const localRequire = Require(localLoader, module); + assert.equal(localRequire.resolve("foo/bar"), "bizzle.js"); + assert.equal(localRequire.resolve("foo/bar/baz"), "bizzle/baz.js"); + assert.equal(localRequire.resolve("foo/bar-bar"), "foo/bar-bar.js"); + assert.equal(localRequire.resolve("foo/bar2/"), "bizzle2.js"); +}; + +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"); +}; + +exports['test lazy globals'] = function (assert) { + let uri = root + '/fixtures/loader/lazy/'; + let gotFoo = false; + let foo = {}; + let modules = { + get foo() { + gotFoo = true; + return foo; + } + }; + let loader = Loader({ paths: { '': uri }, modules: modules}); + assert.ok(!gotFoo, "foo hasn't been accessed during loader instanciation"); + let program = main(loader, 'main'); + assert.ok(!gotFoo, "foo hasn't been accessed during module loading"); + assert.equal(program.useFoo(), foo, "foo mock works"); + assert.ok(gotFoo, "foo has been accessed only when we first try to use it"); +}; + +exports['test user global'] = function(assert) { + // Test case for bug 827792 + let com = {}; + let loader = require('toolkit/loader'); + let loadOptions = require('@loader/options'); + let options = loader.override(loadOptions, + {globals: loader.override(loadOptions.globals, + {com: com, + console: console, + dump: dump})}); + let subloader = loader.Loader(options); + let userRequire = loader.Require(subloader, module); + let userModule = userRequire("./loader/user-global"); + + assert.equal(userModule.getCom(), com, + "user module returns expected `com` global"); +}; + +exports['test custom require caching'] = function(assert) { + const loader = Loader({ + paths: { '': root + "/" }, + requireHook: (id, require) => { + // Just load it normally + return require(id); + } + }); + const require = Require(loader, module); + + let data = require('fixtures/loader/json/mutation.json'); + assert.equal(data.value, 1, 'has initial value'); + data.value = 2; + let newdata = require('fixtures/loader/json/mutation.json'); + assert.equal( + newdata.value, + 2, + 'JSON objects returned should be cached and the same instance' + ); +}; + +exports['test caching when proxying a loader'] = function(assert) { + const parentRequire = require; + const loader = Loader({ + paths: { '': root + "/" }, + requireHook: (id, childRequire) => { + if(id === 'gimmejson') { + return childRequire('fixtures/loader/json/mutation.json') + } + // Load it with the original (global) require + return parentRequire(id); + } + }); + const childRequire = Require(loader, module); + + let data = childRequire('./fixtures/loader/json/mutation.json'); + assert.equal(data.value, 1, 'data has initial value'); + data.value = 2; + + let newdata = childRequire('./fixtures/loader/json/mutation.json'); + assert.equal(newdata.value, 2, 'data has changed'); + + let childData = childRequire('gimmejson'); + assert.equal(childData.value, 1, 'data from child loader has initial value'); + childData.value = 3; + let newChildData = childRequire('gimmejson'); + assert.equal(newChildData.value, 3, 'data from child loader has changed'); + + data = childRequire('./fixtures/loader/json/mutation.json'); + assert.equal(data.value, 2, 'data from parent loader has not changed'); + + // Set it back to the original value just in case (this instance + // will be shared across tests) + data.value = 1; +} + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-match-pattern.js b/addon-sdk/source/test/test-match-pattern.js new file mode 100644 index 000000000..651ad3148 --- /dev/null +++ b/addon-sdk/source/test/test-match-pattern.js @@ -0,0 +1,137 @@ +/* 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 { MatchPattern } = require("sdk/util/match-pattern"); + +exports.testMatchPatternTestTrue = function(assert) { + function ok(pattern, url) { + let mp = new MatchPattern(pattern); + assert.ok(mp.test(url), pattern + " should match " + url); + } + + ok("*", "http://example.com"); + ok("*", "https://example.com"); + ok("*", "ftp://example.com"); + + ok("*.example.com", "http://example.com"); + ok("*.example.com", "http://hamburger.example.com"); + ok("*.example.com", "http://hotdog.hamburger.example.com"); + + ok("http://example.com*", "http://example.com"); + ok("http://example.com*", "http://example.com/"); + ok("http://example.com/*", "http://example.com/"); + ok("http://example.com/*", "http://example.com/potato-salad"); + ok("http://example.com/pickles/*", "http://example.com/pickles/"); + ok("http://example.com/pickles/*", "http://example.com/pickles/lemonade"); + + ok("http://example.com", "http://example.com"); + ok("http://example.com/ice-cream", "http://example.com/ice-cream"); + + ok(/.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok(/.*A.*/i, "http://A.com"); + ok(/.*A.*/i, "http://a.com"); + ok(/https:.*zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok('*.sample.com', 'http://ex.sample.com/foo.html'); + ok('*.amp.le.com', 'http://ex.amp.le.com'); + + ok('data:*', 'data:text/html;charset=utf-8,'); +}; + +exports.testMatchPatternTestFalse = function(assert) { + function ok(pattern, url) { + let mp = new MatchPattern(pattern); + assert.ok(!mp.test(url), pattern + " should not match " + url); + } + + ok("*", null); + ok("*", ""); + ok("*", "bogus"); + ok("*", "chrome://browser/content/browser.xul"); + ok("*", "nttp://example.com"); + + ok("*.example.com", null); + ok("*.example.com", ""); + ok("*.example.com", "bogus"); + ok("*.example.com", "http://example.net"); + ok("*.example.com", "http://foo.com"); + ok("*.example.com", "http://example.com.foo"); + ok("*.example2.com", "http://example.com"); + + ok("http://example.com/*", null); + ok("http://example.com/*", ""); + ok("http://example.com/*", "bogus"); + ok("http://example.com/*", "http://example.com"); + ok("http://example.com/*", "http://foo.com/"); + + ok("http://example.com", null); + ok("http://example.com", ""); + ok("http://example.com", "bogus"); + ok("http://example.com", "http://example.com/"); + + ok(/zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok(/.*zilla/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + ok(/.*Zilla.*/, "https://bugzilla.redhat.com/show_bug.cgi?id=655464"); // bug 655464 + ok(/https:.*zilla/, "https://bugzilla.redhat.com/show_bug.cgi?id=569753"); + + // bug 856913 + ok('*.ign.com', 'http://www.design.com'); + ok('*.ign.com', 'http://design.com'); + ok('*.zilla.com', 'http://bugzilla.mozilla.com'); + ok('*.zilla.com', 'http://mo-zilla.com'); + ok('*.amp.le.com', 'http://amp-le.com'); + ok('*.amp.le.com', 'http://examp.le.com'); +}; + +exports.testMatchPatternErrors = function(assert) { + assert.throws( + () => new MatchPattern("*.google.com/*"), + /There can be at most one/, + "MatchPattern throws when supplied multiple '*'" + ); + + assert.throws( + () => new MatchPattern("google.com"), + /expected to be either an exact URL/, + "MatchPattern throws when the wildcard doesn't use '*' and doesn't " + + "look like a URL" + ); + + assert.throws( + () => new MatchPattern("http://google*.com"), + /expected to be the first or the last/, + "MatchPattern throws when a '*' is in the middle of the wildcard" + ); + + assert.throws( + () => new MatchPattern(/ /g), + /^A RegExp match pattern cannot be set to `global` \(i\.e\. \/\/g\)\.$/, + "MatchPattern throws on a RegExp set to `global` (i.e. //g)." + ); + + assert.throws( + () => new MatchPattern( / /m ), + /^A RegExp match pattern cannot be set to `multiline` \(i\.e\. \/\/m\)\.$/, + "MatchPattern throws on a RegExp set to `multiline` (i.e. //m)." + ); +}; + +exports.testMatchPatternInternals = function(assert) { + assert.equal( + new MatchPattern("http://google.com/test").exactURL, + "http://google.com/test" + ); + + assert.equal( + new MatchPattern("http://google.com/test/*").urlPrefix, + "http://google.com/test/" + ); + + assert.equal( + new MatchPattern("*.example.com").domain, + "example.com" + ); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-method.js b/addon-sdk/source/test/test-method.js new file mode 100644 index 000000000..0478593c1 --- /dev/null +++ b/addon-sdk/source/test/test-method.js @@ -0,0 +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"; + +module.exports = require("method/test/common"); diff --git a/addon-sdk/source/test/test-module.js b/addon-sdk/source/test/test-module.js new file mode 100644 index 000000000..1f9979e4b --- /dev/null +++ b/addon-sdk/source/test/test-module.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"; + +/** Disabled because of Bug 672199 +exports["test module exports are frozen"] = function(assert) { + assert.ok(Object.isFrozen(require("sdk/hotkeys")), + "module exports are frozen"); +}; + +exports["test redefine exported property"] = function(assert) { + let hotkeys = require("sdk/hotkeys"); + let { Hotkey } = hotkeys; + try { Object.defineProperty(hotkeys, 'Hotkey', { value: {} }); } catch(e) {} + assert.equal(hotkeys.Hotkey, Hotkey, "exports can't be redefined"); +}; +*/ + +exports["test can't delete exported property"] = function(assert) { + let hotkeys = require("sdk/hotkeys"); + let { Hotkey } = hotkeys; + + try { delete hotkeys.Hotkey; } catch(e) {} + assert.equal(hotkeys.Hotkey, Hotkey, "exports can't be deleted"); +}; + +exports["test can't override exported property"] = function(assert) { + let hotkeys = require("sdk/hotkeys"); + let { Hotkey } = hotkeys; + + try { hotkeys.Hotkey = Object } catch(e) {} + assert.equal(hotkeys.Hotkey, Hotkey, "exports can't be overriden"); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-modules.js b/addon-sdk/source/test/test-modules.js new file mode 100644 index 000000000..ee9d3d9b5 --- /dev/null +++ b/addon-sdk/source/test/test-modules.js @@ -0,0 +1,150 @@ +/* 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/. */ + +exports.testDefine = function(assert) { + let tiger = require('./modules/tiger'); + assert.equal(tiger.name, 'tiger', 'name proprety was exported properly'); + assert.equal(tiger.type, 'cat', 'property form other module exported'); +}; + +exports.testDefineInoresNonFactory = function(assert) { + let mod = require('./modules/async2'); + assert.equal(mod.name, 'async2', 'name proprety was exported properly'); + assert.ok(mod.traditional2Name !== 'traditional2', '1st is ignored'); +}; +/* Disable test that require AMD specific functionality: + +// define() that exports a function as the module value, +// specifying a module name. +exports.testDefExport = function(assert) { + var add = require('modules/add'); + assert.equal(add(1, 1), 2, 'Named define() exporting a function'); +}; + +// define() that exports function as a value, but is anonymous +exports.testAnonDefExport = function (assert) { + var subtract = require('modules/subtract'); + assert.equal(subtract(4, 2), 2, + 'Anonymous define() exporting a function'); +} + +// using require([], function () {}) to load modules. +exports.testSimpleRequire = function (assert) { + require(['modules/blue', 'modules/orange'], function (blue, orange) { + assert.equal(blue.name, 'blue', 'Simple require for blue'); + assert.equal(orange.name, 'orange', 'Simple require for orange'); + assert.equal(orange.parentType, 'color', + 'Simple require dependency check for orange'); + }); +} + +// using nested require([]) calls. +exports.testSimpleRequireNested = function (assert) { + require(['modules/blue', 'modules/orange', 'modules/green'], + function (blue, orange, green) { + + require(['modules/orange', 'modules/red'], function (orange, red) { + assert.equal(red.name, 'red', 'Simple require for red'); + assert.equal(red.parentType, 'color', + 'Simple require dependency check for red'); + assert.equal(blue.name, 'blue', 'Simple require for blue'); + assert.equal(orange.name, 'orange', 'Simple require for orange'); + assert.equal(orange.parentType, 'color', + 'Simple require dependency check for orange'); + assert.equal(green.name, 'green', 'Simple require for green'); + assert.equal(green.parentType, 'color', + 'Simple require dependency check for green'); + }); + + }); +} + +// requiring a traditional module, that uses async, that use traditional and +// async, with a circular reference +exports.testMixedCircular = function (assert) { + var t = require('modules/traditional1'); + assert.equal(t.name, 'traditional1', 'Testing name'); + assert.equal(t.traditional2Name, 'traditional2', + 'Testing dependent name'); + assert.equal(t.traditional1Name, 'traditional1', 'Testing circular name'); + assert.equal(t.async2Name, 'async2', 'Testing async2 name'); + assert.equal(t.async2Traditional2Name, 'traditional2', + 'Testing nested traditional2 name'); +} + +// Testing define()(function(require) {}) with some that use exports, +// some that use return. +exports.testAnonExportsReturn = function (assert) { + var lion = require('modules/lion'); + require(['modules/tiger', 'modules/cheetah'], function (tiger, cheetah) { + assert.equal('lion', lion, 'Check lion name'); + assert.equal('tiger', tiger.name, 'Check tiger name'); + assert.equal('cat', tiger.type, 'Check tiger type'); + assert.equal('cheetah', cheetah(), 'Check cheetah name'); + }); +} + +// circular dependency +exports.testCircular = function (assert) { + var pollux = require('modules/pollux'), + castor = require('modules/castor'); + + assert.equal(pollux.name, 'pollux', 'Pollux\'s name'); + assert.equal(pollux.getCastorName(), + 'castor', 'Castor\'s name from Pollux.'); + assert.equal(castor.name, 'castor', 'Castor\'s name'); + assert.equal(castor.getPolluxName(), 'pollux', + 'Pollux\'s name from Castor.'); +} + +// test a bad module that asks for exports but also does a define() return +exports.testBadExportAndReturn = function (assert) { + var passed = false; + try { + var bad = require('modules/badExportAndReturn'); + } catch(e) { + passed = /cannot use exports and also return/.test(e.toString()); + } + assert.equal(passed, true, 'Make sure exports and return fail'); +} + +// test a bad circular dependency, where an exported value is needed, but +// the return value happens too late, a module already asked for the exported +// value. +exports.testBadExportAndReturnCircular = function (assert) { + var passed = false; + try { + var bad = require('modules/badFirst'); + } catch(e) { + passed = /after another module has referenced its exported value/ + .test(e.toString()); + } + assert.equal(passed, true, 'Make sure return after an exported ' + + 'value is grabbed by another module fails.'); +} + +// only allow one define call per file. +exports.testOneDefine = function (assert) { + var passed = false; + try { + var dupe = require('modules/dupe'); + } catch(e) { + passed = /Only one call to define/.test(e.toString()); + } + assert.equal(passed, true, 'Only allow one define call per module'); +} + +// only allow one define call per file, testing a bad nested define call. +exports.testOneDefineNested = function (assert) { + var passed = false; + try { + var dupe = require('modules/dupeNested'); + } catch(e) { + passed = /Only one call to define/.test(e.toString()); + } + assert.equal(passed, true, 'Only allow one define call per module'); +} +*/ + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-mozilla-toolkit-versioning.js b/addon-sdk/source/test/test-mozilla-toolkit-versioning.js new file mode 100644 index 000000000..148919595 --- /dev/null +++ b/addon-sdk/source/test/test-mozilla-toolkit-versioning.js @@ -0,0 +1,59 @@ +/* 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 {parse, increment} = require("mozilla-toolkit-versioning/index") + +const TestParse = assert => (version, min, max) => { + const actual = parse(version); + assert.equal(actual.min, min); + assert.equal(actual.max, max); +} + +const TestInc = assert => (version, expected) => { + assert.equal(increment(version), expected, + `increment: ${version} should be equal ${expected}`) +} + + + +exports['test parse(version) single value'] = assert => { + const testParse = TestParse(assert) + testParse('1.2.3', '1.2.3', '1.2.3'); + testParse('>=1.2.3', '1.2.3', undefined); + testParse('<=1.2.3', undefined, '1.2.3'); + testParse('>1.2.3', '1.2.3.1', undefined); + testParse('<1.2.3', undefined, '1.2.3.-1'); + testParse('*', undefined, undefined); +}; + +exports['test parse(version) range'] = assert => { + const testParse = TestParse(assert); + testParse('>=1.2.3 <=2.3.4', '1.2.3', '2.3.4'); + testParse('>1.2.3 <=2.3.4', '1.2.3.1', '2.3.4'); + testParse('>=1.2.3 <2.3.4', '1.2.3', '2.3.4.-1'); + testParse('>1.2.3 <2.3.4', '1.2.3.1', '2.3.4.-1'); + + testParse('<=2.3.4 >=1.2.3', '1.2.3', '2.3.4'); + testParse('<=2.3.4 >1.2.3', '1.2.3.1', '2.3.4'); + testParse('<2.3.4 >=1.2.3', '1.2.3', '2.3.4.-1'); + testParse('<2.3.4 >1.2.3', '1.2.3.1', '2.3.4.-1'); + + testParse('1.2.3pre1 - 2.3.4', '1.2.3pre1', '2.3.4'); +}; + +exports['test increment(version)'] = assert => { + const testInc = TestInc(assert); + + testInc('1.2.3', '1.2.3.1'); + testInc('1.2.3a', '1.2.3a1'); + testInc('1.2.3pre', '1.2.3pre1'); + testInc('1.2.3pre1', '1.2.3pre2'); + testInc('1.2', '1.2.1'); + testInc('1.2pre1a', '1.2pre1b'); + testInc('1.2pre1pre', '1.2pre1prf'); +}; + + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-mpl2-license-header.js b/addon-sdk/source/test/test-mpl2-license-header.js new file mode 100644 index 000000000..22a2cf0ea --- /dev/null +++ b/addon-sdk/source/test/test-mpl2-license-header.js @@ -0,0 +1,105 @@ +// Note: This line is here intentionally, to break MPL2_LICENSE_TEST +/* 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, Ci, Cu } = require("chrome"); +const options = require('@loader/options'); +const { id } = require("sdk/self"); +const { getAddonByID } = require("sdk/addon/manager"); +const { mapcat, map, filter, fromEnumerator } = require("sdk/util/sequence"); +const { readURISync } = require('sdk/net/url'); +const { Request } = require('sdk/request'); +const { defer } = require("sdk/core/promise"); + +const ios = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); + +const MIT_LICENSE_HEADER = []; + +const MPL2_LICENSE_TEST = new RegExp([ + "^\\/\\* 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\\/\\. \\*\\/" +].join("\n")); + +// Note: Using regular expressions because the paths a different for cfx vs jpm +const IGNORES = [ + /lib[\/\\](diffpatcher|method)[\/\\].+$/, // MIT + /lib[\/\\]sdk[\/\\]fs[\/\\]path\.js$/, // MIT + /lib[\/\\]sdk[\/\\]system[\/\\]child_process[\/\\].*/, + /tests?[\/\\]buffers[\/\\].+$/, // MIT + /tests?[\/\\]path[\/\\]test-path\.js$/, + /tests?[\/\\]querystring[\/\\]test-querystring\.js$/, +]; + +const ignoreFile = file => !!IGNORES.find(regex => regex.test(file)); + +const baseURI = "resource://test-sdk-addon/"; + +const uri = (path="") => baseURI + path; + +const toFile = x => x.QueryInterface(Ci.nsIFile); +const isTestFile = ({ path, leafName }) => { + return !ignoreFile(path) && /\.jsm?$/.test(leafName) +}; +const getFileURI = x => ios.newFileURI(x).spec; + +const getDirectoryEntries = file => map(toFile, fromEnumerator(_ => file.directoryEntries)); + +const isDirectory = x => x.isDirectory(); +const getEntries = directory => mapcat(entry => { + if (isDirectory(entry)) { + return getEntries(entry); + } + else if (isTestFile(entry)) { + return [ entry ]; + } + return []; +}, filter(() => true, getDirectoryEntries(directory))); + +function readURL(url) { + let { promise, resolve } = defer(); + + Request({ + url: url, + overrideMimeType: "text/plain", + onComplete: (response) => resolve(response.text) + }).get(); + + return promise; +} + +exports["test MPL2 license header"] = function*(assert) { + let addon = yield getAddonByID(id); + let xpiURI = addon.getResourceURI(); + let rootURL = xpiURI.spec; + assert.ok(rootURL, rootURL); + let files = [...getEntries(xpiURI.QueryInterface(Ci.nsIFileURL).file)]; + + assert.ok(files.length > 1, files.length + " files found."); + let failures = []; + let success = 0; + + for (let i = 0, len = files.length; i < len; i++) { + let file = files[i]; + assert.ok(file.path, "Trying " + file.path); + + const URI = ios.newFileURI(file); + + let leafName = URI.spec.replace(rootURL, ""); + + let contents = yield readURL(URI.spec); + if (!MPL2_LICENSE_TEST.test(contents)) { + failures.push(leafName); + } + } + + assert.equal(1, failures.length, "we expect one failure"); + assert.ok(/test-mpl2-license-header\.js$/.test(failures[0]), "the only failure is this file"); + failures.shift(); + assert.equal("", failures.join(",\n"), failures.length + " files found missing the required mpl 2 header"); +} + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-namespace.js b/addon-sdk/source/test/test-namespace.js new file mode 100644 index 000000000..636682e9e --- /dev/null +++ b/addon-sdk/source/test/test-namespace.js @@ -0,0 +1,120 @@ +/* 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"); +const { Cc, Ci, Cu } = require("chrome"); +const { setTimeout } = require("sdk/timers") + +exports["test post GC references"] = function (assert, done) { + var target = {}, local = ns() + local(target).there = true + + assert.equal(local(target).there, true, "namespaced preserved"); + + Cu.schedulePreciseGC(function() { + assert.equal(local(target).there, true, "namespace is preserved post GC"); + done(); + }); +}; + +exports["test namsepace basics"] = function(assert) { + var privates = ns(); + var object = { foo: function foo() { return "hello foo"; } }; + + assert.notEqual(privates(object), object, + "namespaced object is not the same"); + assert.ok(!('foo' in privates(object)), + "public properties are not in the namespace"); + + assert.equal(privates(object), privates(object), + "same namespaced object is returned on each call"); +}; + +exports["test namespace overlays"] = function(assert) { + var _ = ns(); + var object = { foo: 'foo' }; + + _(object).foo = 'bar'; + + assert.equal(_(object).foo, "bar", + "namespaced property `foo` changed value"); + + assert.equal(object.foo, "foo", + "public property `foo` has original value"); + + object.foo = "baz"; + assert.equal(_(object).foo, "bar", + "property changes do not affect namespaced properties"); + + object.bar = "foo"; + assert.ok(!("bar" in _(object)), + "new public properties are not reflected in namespace"); +}; + +exports["test shared namespaces"] = function(assert) { + var _ = ns(); + + var f1 = { hello: 1 }; + var f2 = { foo: 'foo', hello: 2 }; + _(f1).foo = _(f2).foo = 'bar'; + + assert.equal(_(f1).hello, _(f2).hello, "namespace can be shared"); + assert.notEqual(f1.hello, _(f1).hello, "shared namespace can overlay"); + assert.notEqual(f2.hello, _(f2).hello, "target is not affected"); + + _(f1).hello = 3; + + assert.notEqual(_(f1).hello, _(f2).hello, + "namespaced property can be overided"); + assert.equal(_(f2).hello, _({}).hello, "namespace does not change"); +}; + +exports["test multi namespace"] = function(assert) { + var n1 = ns(); + var n2 = ns(); + var object = { baz: 1 }; + n1(object).foo = 1; + n2(object).foo = 2; + n1(object).bar = n2(object).bar = 3; + + assert.notEqual(n1(object).foo, n2(object).foo, + "object can have multiple namespaces"); + assert.equal(n1(object).bar, n2(object).bar, + "object can have matching props in diff namespaces"); +}; + +exports["test ns alias"] = function(assert) { + assert.strictEqual(ns, require('sdk/core/namespace').Namespace, + "ns is an alias of Namespace"); +}; + +exports["test ns inheritance"] = function(assert) { + let _ = ns(); + + let prototype = { level: 1 }; + let object = Object.create(prototype); + let delegee = Object.create(object); + + _(prototype).foo = {}; + + assert.ok(!Object.prototype.hasOwnProperty.call(_(delegee), "foo"), + "namespaced property is not copied to descendants"); + assert.equal(_(delegee).foo, _(prototype).foo, + "namespaced properties are inherited by descendants"); + + _(object).foo = {}; + assert.notEqual(_(object).foo, _(prototype).foo, + "namespaced properties may be shadowed"); + assert.equal(_(object).foo, _(delegee).foo, + "shadwed properties are inherited by descendants"); + + _(object).bar = {}; + assert.ok(!("bar" in _(prototype)), + "descendants properties are not copied to ancestors"); + assert.ok(_(object).bar, _(delegee).bar, + "descendants properties are inherited"); +}; + +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 new file mode 100644 index 000000000..cc7185522 --- /dev/null +++ b/addon-sdk/source/test/test-native-loader.js @@ -0,0 +1,423 @@ +/* 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 { + Loader, main, unload, parseStack, resolve, nodeResolve +} = require('toolkit/loader'); +var { readURI } = require('sdk/net/url'); +var { all } = require('sdk/core/promise'); +var { before, after } = require('sdk/test/utils'); +var testOptions = require('@test/options'); + +var root = module.uri.substr(0, module.uri.lastIndexOf('/')) +// The following adds Debugger constructor to the global namespace. +const { Cc, Ci, Cu } = require('chrome'); +const { addDebuggerToGlobal } = Cu.import('resource://gre/modules/jsdebugger.jsm', {}); +addDebuggerToGlobal(this); + +const { NetUtil } = Cu.import('resource://gre/modules/NetUtil.jsm', {}); + +const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"] + .getService(Ci.nsIResProtocolHandler); + +const fileRoot = resProto.resolveURI(NetUtil.newURI(root)); + +let variants = [ + { + description: "unpacked resource:", + getRootURI(fixture) { + return `${root}/fixtures/${fixture}/`; + }, + }, + { + description: "unpacked file:", + getRootURI(fixture) { + return `${fileRoot}/fixtures/${fixture}/`; + }, + }, + { + description: "packed resource:", + getRootURI(fixture) { + return `resource://${fixture}/`; + }, + }, + { + description: "packed jar:", + getRootURI(fixture) { + return `jar:${fileRoot}/fixtures/${fixture}.xpi!/`; + }, + }, +]; + +let fixtures = [ + 'native-addon-test', + 'native-overrides-test', +]; + +for (let variant of variants) { + exports[`test nodeResolve (${variant.description})`] = function (assert) { + let rootURI = variant.getRootURI('native-addon-test'); + let manifest = {}; + manifest.dependencies = {}; + + // Handles extensions + resolveTest('../package.json', './dir/c.js', './package.json'); + resolveTest('../dir/b.js', './dir/c.js', './dir/b.js'); + + resolveTest('./dir/b', './index.js', './dir/b.js'); + resolveTest('../index', './dir/b.js', './index.js'); + resolveTest('../', './dir/b.js', './index.js'); + resolveTest('./dir/a', './index.js', './dir/a.js', 'Precedence dir/a.js over dir/a/'); + resolveTest('../utils', './dir/a.js', './utils/index.js', 'Requiring a directory defaults to dir/index.js'); + resolveTest('../newmodule', './dir/c.js', './newmodule/lib/file.js', 'Uses package.json main in dir to load appropriate "main"'); + resolveTest('test-math', './utils/index.js', './node_modules/test-math/index.js', + 'Dependencies default to their index.js'); + resolveTest('test-custom-main', './utils/index.js', './node_modules/test-custom-main/lib/custom-entry.js', + 'Dependencies use "main" entry'); + resolveTest('test-math/lib/sqrt', './utils/index.js', './node_modules/test-math/lib/sqrt.js', + 'Dependencies\' files can be consumed via "/"'); + + resolveTest('sdk/tabs/utils', './index.js', undefined, + 'correctly ignores SDK references in paths'); + resolveTest('fs', './index.js', undefined, + 'correctly ignores built in node modules in paths'); + + resolveTest('test-add', './node_modules/test-math/index.js', + './node_modules/test-math/node_modules/test-add/index.js', + 'Dependencies\' dependencies can be found'); + + resolveTest('resource://gre/modules/commonjs/sdk/tabs.js', './index.js', undefined, + 'correctly ignores absolute URIs.'); + + resolveTest('../tabs', 'resource://gre/modules/commonjs/sdk/addon/bootstrap.js', undefined, + 'correctly ignores attempts to resolve from a module at an absolute URI.'); + + resolveTest('sdk/tabs', 'resource://gre/modules/commonjs/sdk/addon/bootstrap.js', undefined, + 'correctly ignores attempts to resolve from a module at an absolute URI.'); + + function resolveTest (id, requirer, expected, msg) { + let result = nodeResolve(id, requirer, { manifest: manifest, rootURI: rootURI }); + assert.equal(result, expected, 'nodeResolve ' + id + ' from ' + requirer + ' ' +msg); + } + } + + /* + // TODO not working in current env + exports[`test bundle (${variant.description`] = function (assert, done) { + loadAddon('/native-addons/native-addon-test/') + }; + */ + + exports[`test native Loader with mappings (${variant.description})`] = function (assert, done) { + all([ + getJSON('/fixtures/native-addon-test/expectedmap.json'), + getJSON('/fixtures/native-addon-test/package.json') + ]).then(([expectedMap, manifest]) => { + + // Override dummy module and point it to `test-math` to see if the + // require is pulling from the mapping + expectedMap['./index.js']['./dir/dummy'] = './dir/a.js'; + + let rootURI = variant.getRootURI('native-addon-test'); + let loader = Loader({ + paths: makePaths(rootURI), + rootURI: rootURI, + manifest: manifest, + requireMap: expectedMap, + isNative: true + }); + + let program = main(loader); + assert.equal(program.dummyModule, 'dir/a', + 'The lookup uses the information given in the mapping'); + + testLoader(program, assert); + unload(loader); + done(); + }).then(null, (reason) => console.error(reason)); + }; + + exports[`test native Loader overrides (${variant.description})`] = function*(assert) { + const expectedKeys = Object.keys(require("sdk/io/fs")).join(", "); + const manifest = yield getJSON('/fixtures/native-overrides-test/package.json'); + const rootURI = variant.getRootURI('native-overrides-test'); + + let loader = Loader({ + paths: makePaths(rootURI), + rootURI: rootURI, + manifest: manifest, + metadata: manifest, + isNative: true + }); + + let program = main(loader); + let fooKeys = Object.keys(program.foo).join(", "); + let barKeys = Object.keys(program.foo).join(", "); + let fsKeys = Object.keys(program.fs).join(", "); + let overloadKeys = Object.keys(program.overload.fs).join(", "); + let overloadLibKeys = Object.keys(program.overloadLib.fs).join(", "); + + assert.equal(fooKeys, expectedKeys, "foo exports sdk/io/fs"); + assert.equal(barKeys, expectedKeys, "bar exports sdk/io/fs"); + assert.equal(fsKeys, expectedKeys, "sdk/io/fs exports sdk/io/fs"); + assert.equal(overloadKeys, expectedKeys, "overload exports foo which exports sdk/io/fs"); + assert.equal(overloadLibKeys, expectedKeys, "overload/lib/foo exports foo/lib/foo"); + assert.equal(program.internal, "test", "internal exports ./lib/internal"); + assert.equal(program.extra, true, "fs-extra was exported properly"); + + assert.equal(program.Tabs, "no tabs exist", "sdk/tabs exports ./lib/tabs from the add-on"); + assert.equal(program.CoolTabs, "no tabs exist", "sdk/tabs exports ./lib/tabs from the node_modules"); + assert.equal(program.CoolTabsLib, "a cool tabs implementation", "./lib/tabs true relative path from the node_modules"); + + assert.equal(program.ignore, "do not ignore this export", "../ignore override was ignored."); + + unload(loader); + }; + + exports[`test invalid native Loader overrides cause no errors (${variant.description})`] = function*(assert) { + const manifest = yield getJSON('/fixtures/native-overrides-test/package.json'); + const rootURI = variant.getRootURI('native-overrides-test'); + const EXPECTED = JSON.stringify({}); + + let makeLoader = (rootURI, manifest) => Loader({ + paths: makePaths(rootURI), + rootURI: rootURI, + manifest: manifest, + metadata: manifest, + isNative: true + }); + + manifest.jetpack.overrides = "string"; + let loader = makeLoader(rootURI, manifest); + assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED, + "setting jetpack.overrides to a string caused no errors making the loader"); + unload(loader); + + manifest.jetpack.overrides = true; + loader = makeLoader(rootURI, manifest); + assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED, + "setting jetpack.overrides to a boolean caused no errors making the loader"); + unload(loader); + + manifest.jetpack.overrides = 5; + loader = makeLoader(rootURI, manifest); + assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED, + "setting jetpack.overrides to a number caused no errors making the loader"); + unload(loader); + + manifest.jetpack.overrides = null; + loader = makeLoader(rootURI, manifest); + assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED, + "setting jetpack.overrides to null caused no errors making the loader"); + unload(loader); + }; + + exports[`test invalid native Loader jetpack key cause no errors (${variant.description})`] = function*(assert) { + const manifest = yield getJSON('/fixtures/native-overrides-test/package.json'); + const rootURI = variant.getRootURI('native-overrides-test'); + const EXPECTED = JSON.stringify({}); + + let makeLoader = (rootURI, manifest) => Loader({ + paths: makePaths(rootURI), + rootURI: rootURI, + manifest: manifest, + metadata: manifest, + isNative: true + }); + + manifest.jetpack = "string"; + let loader = makeLoader(rootURI, manifest); + assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED, + "setting jetpack.overrides to a string caused no errors making the loader"); + unload(loader); + + manifest.jetpack = true; + loader = makeLoader(rootURI, manifest); + assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED, + "setting jetpack.overrides to a boolean caused no errors making the loader"); + unload(loader); + + manifest.jetpack = 5; + loader = makeLoader(rootURI, manifest); + assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED, + "setting jetpack.overrides to a number caused no errors making the loader"); + unload(loader); + + manifest.jetpack = null; + loader = makeLoader(rootURI, manifest); + assert.equal(JSON.stringify(loader.manifest.jetpack.overrides), EXPECTED, + "setting jetpack.overrides to null caused no errors making the loader"); + unload(loader); + }; + + exports[`test native Loader without mappings (${variant.description})`] = function (assert, done) { + getJSON('/fixtures/native-addon-test/package.json').then(manifest => { + let rootURI = variant.getRootURI('native-addon-test'); + let loader = Loader({ + paths: makePaths(rootURI), + rootURI: rootURI, + manifest: manifest, + isNative: true + }); + + let program = main(loader); + testLoader(program, assert); + unload(loader); + done(); + }).then(null, (reason) => console.error(reason)); + }; + + exports[`test require#resolve with relative, dependencies (${variant.description})`] = function(assert, done) { + getJSON('/fixtures/native-addon-test/package.json').then(manifest => { + let rootURI = variant.getRootURI('native-addon-test'); + let loader = Loader({ + paths: makePaths(rootURI), + rootURI: rootURI, + manifest: manifest, + isNative: true + }); + + let program = main(loader); + let fixtureRoot = program.require.resolve("./").replace(/index\.js$/, ""); + + assert.equal(variant.getRootURI("native-addon-test"), fixtureRoot, "correct resolution root"); + assert.equal(program.require.resolve("test-math"), fixtureRoot + "node_modules/test-math/index.js", "works with node_modules"); + assert.equal(program.require.resolve("./newmodule"), fixtureRoot + "newmodule/lib/file.js", "works with directory mains"); + assert.equal(program.require.resolve("./dir/a"), fixtureRoot + "dir/a.js", "works with normal relative module lookups"); + assert.equal(program.require.resolve("modules/Promise.jsm"), "resource://gre/modules/Promise.jsm", "works with path lookups"); + + // TODO bug 1050422, handle loading non JS/JSM file paths + // assert.equal(program.require.resolve("test-assets/styles.css"), fixtureRoot + "node_modules/test-assets/styles.css", + // "works with different file extension lookups in dependencies"); + + unload(loader); + done(); + }).then(null, (reason) => console.error(reason)); + }; +} + +before(exports, () => { + for (let fixture of fixtures) { + let url = `jar:${root}/fixtures/${fixture}.xpi!/`; + + resProto.setSubstitution(fixture, NetUtil.newURI(url)); + } +}); + +after(exports, () => { + for (let fixture of fixtures) + resProto.setSubstitution(fixture, null); +}); + +exports['test JSM loading'] = function (assert, done) { + getJSON('/fixtures/jsm-package/package.json').then(manifest => { + let rootURI = root + '/fixtures/jsm-package/'; + let loader = Loader({ + paths: makePaths(rootURI), + rootURI: rootURI, + manifest: manifest, + isNative: true + }); + + let program = main(loader); + assert.ok(program.localJSMCached, 'local relative JSMs are cached'); + assert.ok(program.isCachedJSAbsolute , 'absolute resource:// js are cached'); + assert.ok(program.isCachedPath, 'JSMs resolved in paths are cached'); + assert.ok(program.isCachedAbsolute, 'absolute resource:// JSMs are cached'); + + assert.ok(program.localJSM, 'able to load local relative JSMs'); + all([ + program.isLoadedPath(10), + program.isLoadedAbsolute(20), + program.isLoadedJSAbsolute(30) + ]).then(([path, absolute, jsabsolute]) => { + assert.equal(path, 10, 'JSM files resolved from path work'); + assert.equal(absolute, 20, 'JSM files resolved from full resource:// work'); + assert.equal(jsabsolute, 30, 'JS files resolved from full resource:// work'); + }).then(done, console.error); + + }).then(null, console.error); +}; + +function testLoader (program, assert) { + // Test 'main' entries + // no relative custom main `lib/index.js` + assert.equal(program.customMainModule, 'custom entry file', + 'a node_module dependency correctly uses its `main` entry in manifest'); + // relative custom main `./lib/index.js` + assert.equal(program.customMainModuleRelative, 'custom entry file relative', + 'a node_module dependency correctly uses its `main` entry in manifest with relative ./'); + // implicit './index.js' + assert.equal(program.defaultMain, 'default main', + 'a node_module dependency correctly defautls to index.js for main'); + + // Test directory exports + assert.equal(program.directoryDefaults, 'utils', + '`require`ing a directory defaults to dir/index.js'); + assert.equal(program.directoryMain, 'main from new module', + '`require`ing a directory correctly loads the `main` entry and not index.js'); + assert.equal(program.resolvesJSoverDir, 'dir/a', + '`require`ing "a" resolves "a.js" over "a/index.js"'); + + // Test dependency's dependencies + assert.ok(program.math.add, + 'correctly defaults to index.js of a module'); + assert.equal(program.math.add(10, 5), 15, + 'node dependencies correctly include their own dependencies'); + assert.equal(program.math.subtract(10, 5), 5, + 'node dependencies correctly include their own dependencies'); + assert.equal(program.mathInRelative.subtract(10, 5), 5, + 'relative modules can also include node dependencies'); + + // Test SDK natives + assert.ok(program.promise.defer, 'main entry can include SDK modules with no deps'); + assert.ok(program.promise.resolve, 'main entry can include SDK modules with no deps'); + assert.ok(program.eventCore.on, 'main entry can include SDK modules that have dependencies'); + assert.ok(program.eventCore.off, 'main entry can include SDK modules that have dependencies'); + + // Test JSMs + assert.ok(program.promisejsm.defer, 'can require JSM files in path'); + assert.equal(program.localJSM.test, 'this is a jsm', + 'can require relative JSM files'); + + // Other tests + assert.equal(program.areModulesCached, true, + 'modules are correctly cached'); + assert.equal(program.testJSON.dependencies['test-math'], '*', + 'correctly requires JSON files'); +} + +function getJSON (uri) { + return readURI(root + uri).then(manifest => JSON.parse(manifest)); +} + +function makePaths (uri) { + // Uses development SDK modules if overloaded in loader + let sdkPaths = testOptions.paths ? testOptions.paths[''] : 'resource://gre/modules/commonjs/'; + return { + './': uri, + 'sdk/': sdkPaths + 'sdk/', + 'toolkit/': sdkPaths + 'toolkit/', + 'modules/': 'resource://gre/modules/' + }; +} + +function loadAddon (uri, map) { + let rootURI = root + uri; + getJSON(uri + '/package.json').then(manifest => { + let loader = Loader({ + paths: makePaths(rootURI), + rootURI: rootURI, + manifest: manifest, + isNative: true, + modules: { + '@test/options': testOptions + } + }); + let program = main(loader); + }).then(null, console.error); +} + +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 new file mode 100644 index 000000000..84212757a --- /dev/null +++ b/addon-sdk/source/test/test-native-options.js @@ -0,0 +1,183 @@ +/* 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 { setDefaults, injectOptions: inject, validate } = require('sdk/preferences/native-options'); +const { activeBrowserWindow: { document } } = require("sdk/deprecated/window-utils"); +const { preferencesBranch, id } = require('sdk/self'); +const { get } = require('sdk/preferences/service'); +const { setTimeout } = require('sdk/timers'); +const simple = require('sdk/simple-prefs'); +const fixtures = require('./fixtures'); +const { Cc, Ci } = require('chrome'); + +const prefsrv = Cc['@mozilla.org/preferences-service;1']. + getService(Ci.nsIPrefService); + +function injectOptions(preferences, preferencesBranch, document, parent) { + inject({ + id: id, + preferences: preferences, + preferencesBranch: preferencesBranch, + document: document, + parent: parent + }); +} + +exports.testValidate = function(assert) { + let { preferences } = packageJSON('simple-prefs'); + + let block = () => validate(preferences); + + delete preferences[3].options[0].value; + assert.throws(block, /option requires both a value/, "option missing value error"); + + delete preferences[2].options; + assert.throws(block, /'test3' pref requires options/, "menulist missing options error"); + + preferences[1].type = 'control'; + assert.throws(block, /'test2' control requires a label/, "control missing label error"); + + preferences[1].type = 'nonvalid'; + assert.throws(block, /'test2' pref must be of valid type/, "invalid pref type error"); + + delete preferences[0].title; + assert.throws(block, /'test' pref requires a title/, "pref missing title error"); +} + +exports.testNoPrefs = function(assert, done) { + let { preferences } = packageJSON('no-prefs'); + + let parent = document.createDocumentFragment(); + injectOptions(preferences || [], preferencesBranch, document, parent); + assert.equal(parent.children.length, 0, "No setting elements injected"); + + // must test with events because we can't reset default prefs + function onPrefChange(name) { + assert.fail("No preferences should be defined"); + } + + simple.on('', onPrefChange); + setDefaults(preferences || [], preferencesBranch); + setTimeout(function() { + assert.pass("No preferences were defined"); + simple.off('', onPrefChange); + done(); + }, 100); +} + +exports.testCurlyID = function(assert) { + let { preferences, id } = packageJSON('curly-id'); + let branch = prefsrv.getDefaultBranch('extensions.' + id); + + let parent = document.createDocumentFragment(); + injectOptions(preferences, id, document, parent); + assert.equal(parent.children.length, 1, "One setting elements injected"); + assert.equal(parent.firstElementChild.attributes.pref.value, + "extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13", + "Setting pref attribute is set properly"); + + setDefaults(preferences, id); + assert.equal(get('extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13'), + 26, "test13 is 26"); + + branch.deleteBranch(''); + assert.equal(get('extensions.{34a1eae1-c20a-464f-9b0e-000000000000}.test13'), + undefined, "test13 is undefined"); +} + +exports.testPreferencesBranch = function(assert) { + let { preferences, 'preferences-branch': prefsBranch } = packageJSON('preferences-branch'); + let branch = prefsrv.getDefaultBranch('extensions.' + prefsBranch); + + let parent = document.createDocumentFragment(); + injectOptions(preferences, prefsBranch, document, parent); + assert.equal(parent.children.length, 1, "One setting elements injected"); + assert.equal(parent.firstElementChild.attributes.pref.value, + "extensions.human-readable.test42", + "Setting pref attribute is set properly"); + + setDefaults(preferences, prefsBranch); + assert.equal(get('extensions.human-readable.test42'), true, "test42 is true"); + + branch.deleteBranch(''); + assert.equal(get('extensions.human-readable.test42'), undefined, "test42 is undefined"); +} + +exports.testSimplePrefs = function(assert) { + let { preferences } = packageJSON('simple-prefs'); + let branch = prefsrv.getDefaultBranch('extensions.' + preferencesBranch); + + 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, + "setting 'pref' attribute correct"); + assert.equal(setting.getAttribute('pref-name'), name, + "setting 'pref-name' attribute correct"); + assert.equal(setting.getAttribute('type'), type, + "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) { + assert.equal(option.getAttribute('value'), value, "value attribute correct"); + assert.equal(option.getAttribute('label'), label, "label attribute correct"); + } + + let parent = document.createDocumentFragment(); + injectOptions(preferences, preferencesBranch, document, parent); + assert.equal(parent.children.length, 8, "Eight setting elements injected"); + + 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', '">menuitem'); + let radios = parent.children[3].querySelectorAll('radiogroup>radio'); + + assertOption(menuItems[0], '0', 'label1'); + assertOption(menuItems[1], '1', 'label2'); + assertOption(radios[0], 'red', 'rouge'); + assertOption(radios[1], 'blue', 'bleu'); + + setDefaults(preferences, preferencesBranch); + assert.strictEqual(simple.prefs.test, false, "test is false"); + assert.strictEqual(simple.prefs.test2, "\u00FCnic\u00F8d\u00E9", "test2 is unicode"); + assert.strictEqual(simple.prefs.test3, "1", "test3 is '1'"); + assert.strictEqual(simple.prefs.test4, "red", "test4 is 'red'"); + + // Only delete the test preferences to avoid unsetting any test harness + // preferences. + for (let setting of parent.children) { + let name = setting.getAttribute('pref-name'); + branch.deleteBranch("." + name); + } + + assert.strictEqual(simple.prefs.test, undefined, "test is undefined"); + assert.strictEqual(simple.prefs.test2, undefined, "test2 is undefined"); + assert.strictEqual(simple.prefs.test3, undefined, "test3 is undefined"); + assert.strictEqual(simple.prefs.test4, undefined, "test4 is undefined"); +} + +function packageJSON(dir) { + return require(fixtures.url('preferences/' + dir + '/package.json')); +} + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-net-url.js b/addon-sdk/source/test/test-net-url.js new file mode 100644 index 000000000..9e463b798 --- /dev/null +++ b/addon-sdk/source/test/test-net-url.js @@ -0,0 +1,137 @@ +/* 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 { readURI, readURISync } = require("sdk/net/url"); +const data = require("./fixtures"); + +const utf8text = "Hello, ゼロ!"; +const latin1text = "Hello, ゼロ!"; + +const dataURIutf8 = "data:text/plain;charset=utf-8," + encodeURIComponent(utf8text); +const dataURIlatin1 = "data:text/plain;charset=ISO-8859-1," + escape(latin1text); +const chromeURI = "chrome://global-platform/locale/accessible.properties"; + +exports["test async readURI"] = function(assert, done) { + let content = ""; + + readURI(data.url("test-net-url.txt")).then(function(data) { + content = data; + assert.equal(content, utf8text, "The URL content is loaded properly"); + done(); + }, function() { + assert.fail("should not reject"); + done(); + }) + + assert.equal(content, "", "The URL content is not load yet"); +} + +exports["test readURISync"] = function(assert) { + let content = readURISync(data.url("test-net-url.txt")); + + assert.equal(content, utf8text, "The URL content is loaded properly"); +} + +exports["test async readURI with ISO-8859-1 charset"] = function(assert, done) { + let content = ""; + + readURI(data.url("test-net-url.txt"), { charset : "ISO-8859-1"}).then(function(data) { + content = data; + assert.equal(content, latin1text, "The URL content is loaded properly"); + done(); + }, function() { + assert.fail("should not reject"); + done(); + }) + + assert.equal(content, "", "The URL content is not load yet"); +} + +exports["test readURISync with ISO-8859-1 charset"] = function(assert) { + let content = readURISync(data.url("test-net-url.txt"), "ISO-8859-1"); + + assert.equal(content, latin1text, "The URL content is loaded properly"); +} + +exports["test async readURI with not existing file"] = function(assert, done) { + readURI(data.url("test-net-url-fake.txt")).then(function(data) { + assert.fail("should not resolve"); + done(); + }, function(reason) { + assert.ok(reason.indexOf("Failed to read:") === 0); + done(); + }) +} + +exports["test readURISync with not existing file"] = function(assert) { + assert.throws(function() { + readURISync(data.url("test-net-url-fake.txt")); + }, /NS_ERROR_FILE_NOT_FOUND/); +} + +exports["test async readURI with data URI"] = function(assert, done) { + let content = ""; + + readURI(dataURIutf8).then(function(data) { + content = data; + assert.equal(content, utf8text, "The URL content is loaded properly"); + done(); + }, function() { + assert.fail("should not reject"); + done(); + }) + + assert.equal(content, "", "The URL content is not load yet"); +} + +exports["test readURISync with data URI"] = function(assert) { + let content = readURISync(dataURIutf8); + + assert.equal(content, utf8text, "The URL content is loaded properly"); +} + +exports["test async readURI with data URI and ISO-8859-1 charset"] = function(assert, done) { + let content = ""; + + readURI(dataURIlatin1, { charset : "ISO-8859-1"}).then(function(data) { + content = unescape(data); + assert.equal(content, latin1text, "The URL content is loaded properly"); + done(); + }, function() { + assert.fail("should not reject"); + done(); + }) + + assert.equal(content, "", "The URL content is not load yet"); +} + +exports["test readURISync with data URI and ISO-8859-1 charset"] = function(assert) { + let content = unescape(readURISync(dataURIlatin1, "ISO-8859-1")); + + assert.equal(content, latin1text, "The URL content is loaded properly"); +} + +exports["test readURISync with chrome URI"] = function(assert) { + let content = readURISync(chromeURI); + + assert.ok(content, "The URL content is loaded properly"); +} + +exports["test async readURI with chrome URI"] = function(assert, done) { + let content = ""; + + readURI(chromeURI).then(function(data) { + content = data; + assert.equal(content, readURISync(chromeURI), "The URL content is loaded properly"); + done(); + }, function() { + assert.fail("should not reject"); + done(); + }) + + assert.equal(content, "", "The URL content is not load yet"); +} + +require("sdk/test").run(exports) diff --git a/addon-sdk/source/test/test-node-os.js b/addon-sdk/source/test/test-node-os.js new file mode 100644 index 000000000..e8316d7d9 --- /dev/null +++ b/addon-sdk/source/test/test-node-os.js @@ -0,0 +1,33 @@ +/* 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 os = require("node/os"); +var system = require("sdk/system"); + +exports["test os"] = function (assert) { + assert.equal(os.tmpdir(), system.pathFor("TmpD"), "os.tmpdir() matches temp dir"); + assert.ok(os.endianness() === "BE" || os.endianness() === "LE", "os.endianness is BE or LE"); + + assert.ok(os.arch().length > 0, "os.arch() returns a value"); + assert.equal(typeof os.arch(), "string", "os.arch() returns a string"); + assert.ok(os.type().length > 0, "os.type() returns a value"); + assert.equal(typeof os.type(), "string", "os.type() returns a string"); + assert.ok(os.platform().length > 0, "os.platform() returns a value"); + assert.equal(typeof os.platform(), "string", "os.platform() returns a string"); + + assert.ok(os.release().length > 0, "os.release() returns a value"); + assert.equal(typeof os.release(), "string", "os.release() returns a string"); + assert.ok(os.hostname().length > 0, "os.hostname() returns a value"); + assert.equal(typeof os.hostname(), "string", "os.hostname() returns a string"); + assert.ok(os.EOL === "\n" || os.EOL === "\r\n", "os.EOL returns a correct EOL char"); + + assert.deepEqual(os.loadavg(), [0, 0, 0], "os.loadavg() returns an array of 0s"); + + ["uptime", "totalmem", "freemem", "cpus"].forEach(method => { + assert.throws(() => os[method](), "os." + method + " throws"); + }); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-notifications.js b/addon-sdk/source/test/test-notifications.js new file mode 100644 index 000000000..9a4d6cea0 --- /dev/null +++ b/addon-sdk/source/test/test-notifications.js @@ -0,0 +1,94 @@ +/* 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('sdk/test/loader'); + +exports["test onClick"] = function(assert) { + let [loader, mockAlertServ] = makeLoader(module); + let notifs = loader.require("sdk/notifications"); + let data = "test data"; + let opts = { + onClick: function (clickedData) { + assert.equal(this, notifs, "|this| should be notifications module"); + assert.equal(clickedData, data, + "data passed to onClick should be correct"); + }, + data: data, + title: "test title", + text: "test text", + iconURL: "test icon URL" + }; + notifs.notify(opts); + mockAlertServ.click(); + loader.unload(); +}; + +exports['test numbers and URLs in options'] = function(assert) { + let [loader] = makeLoader(module); + let notifs = loader.require('sdk/notifications'); + let opts = { + title: 123, + text: 45678, + // must use in-loader `sdk/url` module for the validation type check to work + iconURL: loader.require('sdk/url').URL('data:image/png,blah') + }; + try { + notifs.notify(opts); + assert.pass('using numbers and URLs in options works'); + } catch (e) { + assert.fail('using numbers and URLs in options must not throw'); + } + loader.unload(); +} + +exports['test new tag, dir and lang options'] = function(assert) { + let [loader] = makeLoader(module); + let notifs = loader.require('sdk/notifications'); + let opts = { + title: 'best', + tag: 'tagging', + lang: 'en' + }; + + try { + opts.dir = 'ttb'; + notifs.notify(opts); + assert.fail('`dir` option must not accept TopToBottom direction.'); + } catch (e) { + assert.equal(e.message, + '`dir` option must be one of: "auto", "ltr" or "rtl".'); + } + + try { + opts.dir = 'rtl'; + notifs.notify(opts); + assert.pass('`dir` option accepts "rtl" direction.'); + } catch (e) { + assert.fail('`dir` option must accept "rtl" direction.'); + } + + loader.unload(); +} + +// Returns [loader, mockAlertService]. +function makeLoader(module) { + let loader = Loader(module); + let mockAlertServ = { + showAlertNotification: function (imageUrl, title, text, textClickable, + cookie, alertListener, name) { + this._cookie = cookie; + this._alertListener = alertListener; + }, + click: function () { + this._alertListener.observe(null, "alertclickcallback", this._cookie); + } + }; + loader.require("sdk/notifications"); + let scope = loader.sandbox("sdk/notifications"); + scope.notify = mockAlertServ.showAlertNotification.bind(mockAlertServ); + return [loader, mockAlertServ]; +} + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-object.js b/addon-sdk/source/test/test-object.js new file mode 100644 index 000000000..a380fac8a --- /dev/null +++ b/addon-sdk/source/test/test-object.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'; + +const { merge, extend, has, each } = require('sdk/util/object'); + +var o = { + 'paper': 0, + 'rock': 1, + 'scissors': 2 +}; + +//exports.testMerge = function(assert) {} +//exports.testExtend = function(assert) {} + +exports.testHas = function(assert) { + assert.equal(has(o, 'paper'), true, 'has correctly finds key'); + assert.equal(has(o, 'rock'), true, 'has correctly finds key'); + assert.equal(has(o, 'scissors'), true, 'has correctly finds key'); + assert.equal(has(o, 'nope'), false, 'has correctly does not find key'); + assert.equal(has(o, '__proto__'), false, 'has correctly does not find key'); + assert.equal(has(o, 'isPrototypeOf'), false, 'has correctly does not find key'); +}; + +exports.testEach = function(assert) { + var keys = new Set(); + each(o, function (value, key, object) { + keys.add(key); + assert.equal(o[key], value, 'Key and value pairs passed in'); + assert.equal(o, object, 'Object passed in'); + }); + assert.equal(keys.size, 3, 'All keys have been iterated upon'); +}; + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-observers.js b/addon-sdk/source/test/test-observers.js new file mode 100644 index 000000000..4f15a87f1 --- /dev/null +++ b/addon-sdk/source/test/test-observers.js @@ -0,0 +1,183 @@ +/* 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("sdk/test/loader"); +const { isWeak, WeakReference } = require("sdk/core/reference"); +const { subscribe, unsubscribe, + observe, Observer } = require("sdk/core/observer"); +const { Class } = require("sdk/core/heritage"); + +const { Cc, Ci, Cu } = require("chrome"); +const { notifyObservers } = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); +const { defer } = require("sdk/core/promise"); + +const message = x => ({wrappedJSObject: x}); + +exports["test subscribe unsubscribe"] = assert => { + const topic = Date.now().toString(32); + const Subscriber = Class({ + extends: Observer, + initialize: function(observe) { + this.observe = observe; + } + }); + observe.define(Subscriber, (x, subject, _, data) => + x.observe(subject.wrappedJSObject.x)); + + let xs = []; + const x = Subscriber((...rest) => xs.push(...rest)); + + let ys = []; + const y = Subscriber((...rest) => ys.push(...rest)); + + const publish = (topic, data) => + notifyObservers(message(data), topic, null); + + publish({x:0}); + + subscribe(x, topic); + + publish(topic, {x:1}); + + subscribe(y, topic); + + publish(topic, {x:2}); + publish(topic + "!", {x: 2.5}); + + unsubscribe(x, topic); + + publish(topic, {x:3}); + + subscribe(y, topic); + + publish(topic, {x:4}); + + subscribe(x, topic); + + publish(topic, {x:5}); + + unsubscribe(x, topic); + unsubscribe(y, topic); + + publish(topic, {x:6}); + + assert.deepEqual(xs, [1, 2, 5]); + assert.deepEqual(ys, [2, 3, 4, 5]); +} + +exports["test weak observers are GC-ed on unload"] = (assert, end) => { + const topic = Date.now().toString(32); + const loader = Loader(module); + const { Observer, observe, + subscribe, unsubscribe } = loader.require("sdk/core/observer"); + const { isWeak, WeakReference } = loader.require("sdk/core/reference"); + + const MyObserver = Class({ + extends: Observer, + initialize: function(observe) { + this.observe = observe; + } + }); + observe.define(MyObserver, (x, ...rest) => x.observe(...rest)); + + const MyWeakObserver = Class({ + extends: MyObserver, + implements: [WeakReference] + }); + + let xs = []; + let ys = []; + let x = new MyObserver((subject, topic, data) => { + xs.push(subject.wrappedJSObject, topic, data); + }); + let y = new MyWeakObserver((subject, topic, data) => { + ys.push(subject.wrappedJSObject, topic, data); + }); + + subscribe(x, topic); + subscribe(y, topic); + + + notifyObservers(message({ foo: 1 }), topic, null); + x = null; + y = null; + loader.unload(); + + Cu.schedulePreciseGC(() => { + + notifyObservers(message({ bar: 2 }), topic, ":)"); + + assert.deepEqual(xs, [{ foo: 1 }, topic, null, + { bar: 2 }, topic, ":)"], + "non week observer is kept"); + + assert.deepEqual(ys, [{ foo: 1 }, topic, null], + "week observer was GC-ed"); + + end(); + }); +}; + +exports["test weak observer unsubscribe"] = function*(assert) { + const loader = Loader(module); + const { Observer, observe, subscribe, unsubscribe } = loader.require("sdk/core/observer"); + const { WeakReference } = loader.require("sdk/core/reference"); + + let sawNotification = false; + let firstWait = defer(); + let secondWait = defer(); + + const WeakObserver = Class({ + extends: Observer, + implements: [WeakReference], + observe: function() { + sawNotification = true; + firstWait.resolve(); + } + }); + + const StrongObserver = Class({ + extends: Observer, + observe: function() { + secondWait.resolve(); + } + }); + + observe.define(Observer, (x, ...rest) => x.observe(...rest)); + + let weakObserver = new WeakObserver; + let strongObserver = new StrongObserver(); + subscribe(weakObserver, "test-topic"); + subscribe(strongObserver, "test-wait"); + + notifyObservers(null, "test-topic", null); + yield firstWait.promise; + + assert.ok(sawNotification, "Should have seen notification before GC"); + sawNotification = false; + + yield loader.require("sdk/test/memory").gc(); + + notifyObservers(null, "test-topic", null); + notifyObservers(null, "test-wait", null); + yield secondWait.promise; + + assert.ok(sawNotification, "Should have seen notification after GC"); + sawNotification = false; + + try { + unsubscribe(weakObserver, "test-topic"); + unsubscribe(strongObserver, "test-wait"); + assert.pass("Should not have seen an exception"); + } + catch (e) { + assert.fail("Should not have seen an exception"); + } + + loader.unload(); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-page-mod-debug.js b/addon-sdk/source/test/test-page-mod-debug.js new file mode 100644 index 000000000..86f491149 --- /dev/null +++ b/addon-sdk/source/test/test-page-mod-debug.js @@ -0,0 +1,66 @@ +/* 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, Ci, Cu } = require("chrome"); +const { PageMod } = require("sdk/page-mod"); +const { testPageMod, handleReadyState, openNewTab, + contentScriptWhenServer, createLoader } = require("./page-mod/helpers"); +const { cleanUI, after } = require("sdk/test/utils"); +const { open, getFrames, getMostRecentBrowserWindow, getInnerId } = require("sdk/window/utils"); + +const { devtools } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const { require: devtoolsRequire } = devtools; +const contentGlobals = devtoolsRequire("devtools/server/content-globals"); + +// The following adds Debugger constructor to the global namespace. +const { addDebuggerToGlobal } = require('resource://gre/modules/jsdebugger.jsm'); +addDebuggerToGlobal(this); + +exports.testDebugMetadata = function(assert, done) { + let dbg = new Debugger; + let globalDebuggees = []; + dbg.onNewGlobalObject = function(global) { + globalDebuggees.push(global); + } + + let mods = testPageMod(assert, done, "about:", [{ + include: "about:", + contentScriptWhen: "start", + contentScript: "null;", + }], function(win, done) { + assert.ok(globalDebuggees.some(function(global) { + try { + let metadata = Cu.getSandboxMetadata(global.unsafeDereference()); + return metadata && metadata.addonID && metadata.SDKContentScript && + metadata['inner-window-id'] == getInnerId(win); + } catch(e) { + // Some of the globals might not be Sandbox instances and thus + // will cause getSandboxMetadata to fail. + return false; + } + }), "one of the globals is a content script"); + done(); + } + ); +}; + +exports.testDevToolsExtensionsGetContentGlobals = function(assert, done) { + let mods = testPageMod(assert, done, "about:", [{ + include: "about:", + contentScriptWhen: "start", + contentScript: "null;", + }], function(win, done) { + assert.equal(contentGlobals.getContentGlobals({ 'inner-window-id': getInnerId(win) }).length, 1); + done(); + } + ); +}; + +after(exports, function*(name, assert) { + assert.pass("cleaning ui."); + yield cleanUI(); +}); + +require('sdk/test').run(exports); diff --git a/addon-sdk/source/test/test-page-mod.js b/addon-sdk/source/test/test-page-mod.js new file mode 100644 index 000000000..d03463d2d --- /dev/null +++ b/addon-sdk/source/test/test-page-mod.js @@ -0,0 +1,2214 @@ +/* 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, Ci, Cu } = require("chrome"); +const { PageMod } = require("sdk/page-mod"); +const { testPageMod, handleReadyState, openNewTab, + contentScriptWhenServer, createLoader } = require("./page-mod/helpers"); +const { Loader } = require("sdk/test/loader"); +const tabs = require("sdk/tabs"); +const { setTimeout } = require("sdk/timers"); +const system = require("sdk/system/events"); +const { open, getFrames, getMostRecentBrowserWindow, getInnerId } = require("sdk/window/utils"); +const { getTabContentWindow, getActiveTab, setTabURL, openTab, closeTab, + getBrowserForTab } = require("sdk/tabs/utils"); +const xulApp = require("sdk/system/xul-app"); +const { isPrivateBrowsingSupported } = require("sdk/self"); +const { isPrivate } = require("sdk/private-browsing"); +const { openWebpage } = require("./private-browsing/helper"); +const { isTabPBSupported, isWindowPBSupported } = require("sdk/private-browsing/utils"); +const promise = require("sdk/core/promise"); +const { pb } = require("./private-browsing/helper"); +const { URL } = require("sdk/url"); +const { defer, all, resolve } = require("sdk/core/promise"); +const { waitUntil } = require("sdk/test/utils"); +const data = require("./fixtures"); +const { cleanUI, after } = require("sdk/test/utils"); + +const testPageURI = data.url("test.html"); + +function Isolate(worker) { + return "(" + worker + ")()"; +} + +/* Tests for the PageMod APIs */ + +exports.testPageMod1 = function*(assert) { + let modAttached = defer(); + let mod = PageMod({ + include: /about:/, + contentScriptWhen: "end", + contentScript: "new " + function WorkerScope() { + window.document.body.setAttribute("JEP-107", "worked"); + + self.port.once("done", () => { + self.port.emit("results", window.document.body.getAttribute("JEP-107")) + }); + }, + onAttach: function(worker) { + assert.equal(this, mod, "The 'this' object is the page mod."); + mod.port.once("results", modAttached.resolve) + mod.port.emit("done"); + } + }); + + let tab = yield new Promise(resolve => { + tabs.open({ + url: "about:", + inBackground: true, + onReady: resolve + }) + }); + assert.pass("test tab was opened."); + + let worked = yield modAttached.promise; + assert.pass("test mod was attached."); + + mod.destroy(); + assert.pass("test mod was destroyed."); + + assert.equal(worked, "worked", "PageMod.onReady test"); +}; + +exports.testPageMod2 = function*(assert) { + let modAttached = defer(); + let mod = PageMod({ + include: testPageURI, + contentScriptWhen: "end", + contentScript: [ + 'new ' + function contentScript() { + window.AUQLUE = function() { return 42; } + try { + window.AUQLUE() + } + catch(e) { + throw new Error("PageMod scripts executed in order"); + } + document.documentElement.setAttribute("first", "true"); + }, + 'new ' + function contentScript() { + document.documentElement.setAttribute("second", "true"); + + self.port.once("done", () => { + self.port.emit("results", { + "first": window.document.documentElement.getAttribute("first"), + "second": window.document.documentElement.getAttribute("second"), + "AUQLUE": unsafeWindow.getAUQLUE() + }); + }); + } + ], + onAttach: modAttached.resolve + }); + + let tab = yield new Promise(resolve => { + tabs.open({ + url: testPageURI, + inBackground: true, + onReady: resolve + }) + }); + assert.pass("test tab was opened."); + + let worker = yield modAttached.promise; + assert.pass("test mod was attached."); + + let results = yield new Promise(resolve => { + worker.port.once("results", resolve) + worker.port.emit("done"); + }); + + mod.destroy(); + assert.pass("test mod was destroyed."); + + assert.equal(results["first"], + "true", + "PageMod test #2: first script has run"); + assert.equal(results["second"], + "true", + "PageMod test #2: second script has run"); + assert.equal(results["AUQLUE"], false, + "PageMod test #2: scripts get a wrapped window"); +}; + +exports.testPageModIncludes = function*(assert) { + var modsAttached = []; + var modNumber = 0; + var modAttached = defer(); + let includes = [ + "*", + "*.google.com", + "resource:*", + "resource:", + testPageURI + ]; + let expected = [ + false, + false, + true, + false, + true + ] + + let mod = PageMod({ + include: testPageURI, + contentScript: 'new ' + function() { + self.port.on("get-local-storage", () => { + let result = {}; + self.options.forEach(include => { + result[include] = !!window.localStorage[include] + }); + + self.port.emit("got-local-storage", result); + + window.localStorage.clear(); + }); + }, + contentScriptOptions: includes, + onAttach: modAttached.resolve + }); + + function createPageModTest(include, expectedMatch) { + var modIndex = modNumber++; + + let attached = defer(); + modsAttached.push(expectedMatch ? attached.promise : resolve()); + + // ...and corresponding PageMod options + return PageMod({ + include: include, + contentScript: 'new ' + function() { + self.on("message", function(msg) { + window.localStorage[msg] = true + self.port.emit('done'); + }); + }, + // The testPageMod callback with test assertions is called on 'end', + // and we want this page mod to be attached before it gets called, + // so we attach it on 'start'. + contentScriptWhen: 'start', + onAttach: function(worker) { + assert.pass("mod " + modIndex + " was attached"); + + worker.port.once("done", () => { + assert.pass("mod " + modIndex + " is done"); + attached.resolve(worker); + }); + worker.postMessage(this.include[0]); + } + }); + } + + let mods = [ + createPageModTest("*", false), + createPageModTest("*.google.com", false), + createPageModTest("resource:*", true), + createPageModTest("resource:", false), + createPageModTest(testPageURI, true) + ]; + + let tab = yield new Promise(resolve => { + tabs.open({ + url: testPageURI, + inBackground: true, + onReady: resolve + }); + }); + assert.pass("tab was opened"); + + yield all(modsAttached); + assert.pass("all mods were attached."); + + mods.forEach(mod => mod.destroy()); + assert.pass("all mods were destroyed."); + + yield modAttached.promise; + assert.pass("final test mod was attached."); + + yield new Promise(resolve => { + mod.port.on("got-local-storage", (storage) => { + includes.forEach((include, i) => { + assert.equal(storage[include], expected[i], "localStorage is correct for " + include); + }); + resolve(); + }); + mod.port.emit("get-local-storage"); + }); + assert.pass("final test of localStorage is complete."); + + mod.destroy(); + assert.pass("final test mod was destroyed."); +}; + +exports.testPageModExcludes = function(assert, done) { + var asserts = []; + function createPageModTest(include, exclude, expectedMatch) { + // Create an 'onload' test function... + asserts.push(function(test, win) { + var matches = JSON.stringify([include, exclude]) in win.localStorage; + assert.ok(expectedMatch ? matches : !matches, + "[include, exclude] = [" + include + ", " + exclude + + "] match test, expected: " + expectedMatch); + }); + // ...and corresponding PageMod options + return { + include: include, + exclude: exclude, + contentScript: 'new ' + function() { + self.on("message", function(msg) { + // The key in localStorage is "[, ]". + window.localStorage[JSON.stringify(msg)] = true; + }); + }, + // The testPageMod callback with test assertions is called on 'end', + // and we want this page mod to be attached before it gets called, + // so we attach it on 'start'. + contentScriptWhen: 'start', + onAttach: function(worker) { + worker.postMessage([this.include[0], this.exclude[0]]); + } + }; + } + + testPageMod(assert, done, testPageURI, [ + createPageModTest("*", testPageURI, false), + createPageModTest(testPageURI, testPageURI, false), + createPageModTest(testPageURI, "resource://*", false), + createPageModTest(testPageURI, "*.google.com", true) + ], + function (win, done) { + waitUntil(() => win.localStorage[JSON.stringify([testPageURI, "*.google.com"])], + testPageURI + " page-mod to be executed") + .then(() => { + asserts.forEach(fn => fn(assert, win)); + win.localStorage.clear(); + done(); + }); + }); +}; + +exports.testPageModValidationAttachTo = function(assert) { + [{ val: 'top', type: 'string "top"' }, + { val: 'frame', type: 'string "frame"' }, + { val: ['top', 'existing'], type: 'array with "top" and "existing"' }, + { val: ['frame', 'existing'], type: 'array with "frame" and "existing"' }, + { val: ['top'], type: 'array with "top"' }, + { val: ['frame'], type: 'array with "frame"' }, + { val: undefined, type: 'undefined' }].forEach((attachTo) => { + new PageMod({ attachTo: attachTo.val, include: '*.validation111' }); + assert.pass("PageMod() does not throw when attachTo is " + attachTo.type); + }); + + [{ val: 'existing', type: 'string "existing"' }, + { val: ['existing'], type: 'array with "existing"' }, + { val: 'not-legit', type: 'string with "not-legit"' }, + { val: ['not-legit'], type: 'array with "not-legit"' }, + { val: {}, type: 'object' }].forEach((attachTo) => { + assert.throws(() => + new PageMod({ attachTo: attachTo.val, include: '*.validation111' }), + /The `attachTo` option/, + "PageMod() throws when 'attachTo' option is " + attachTo.type + "."); + }); +}; + +exports.testPageModValidationInclude = function(assert) { + [{ val: undefined, type: 'undefined' }, + { val: {}, type: 'object' }, + { val: [], type: 'empty array'}, + { val: [/regexp/, 1], type: 'array with non string/regexp' }, + { val: 1, type: 'number' }].forEach((include) => { + assert.throws(() => new PageMod({ include: include.val }), + /The `include` option must always contain atleast one rule/, + "PageMod() throws when 'include' option is " + include.type + "."); + }); + + [{ val: '*.validation111', type: 'string' }, + { val: /validation111/, type: 'regexp' }, + { val: ['*.validation111'], type: 'array with length > 0'}].forEach((include) => { + new PageMod({ include: include.val }); + assert.pass("PageMod() does not throw when include option is " + include.type); + }); +}; + +exports.testPageModValidationExclude = function(assert) { + let includeVal = '*.validation111'; + + [{ val: {}, type: 'object' }, + { val: [], type: 'empty array'}, + { val: [/regexp/, 1], type: 'array with non string/regexp' }, + { val: 1, type: 'number' }].forEach((exclude) => { + assert.throws(() => new PageMod({ include: includeVal, exclude: exclude.val }), + /If set, the `exclude` option must always contain at least one rule as a string, regular expression, or an array of strings and regular expressions./, + "PageMod() throws when 'exclude' option is " + exclude.type + "."); + }); + + [{ val: undefined, type: 'undefined' }, + { val: '*.validation111', type: 'string' }, + { val: /validation111/, type: 'regexp' }, + { val: ['*.validation111'], type: 'array with length > 0'}].forEach((exclude) => { + new PageMod({ include: includeVal, exclude: exclude.val }); + assert.pass("PageMod() does not throw when exclude option is " + exclude.type); + }); +}; + +/* Tests for internal functions. */ +exports.testCommunication1 = function*(assert) { + let workerDone = defer(); + + let mod = PageMod({ + include: "about:*", + contentScriptWhen: "end", + contentScript: 'new ' + function WorkerScope() { + self.on('message', function(msg) { + document.body.setAttribute('JEP-107', 'worked'); + self.postMessage(document.body.getAttribute('JEP-107')); + }); + self.port.on('get-jep-107', () => { + self.port.emit('got-jep-107', document.body.getAttribute('JEP-107')); + }); + }, + onAttach: function(worker) { + worker.on('error', function(e) { + assert.fail('Errors where reported'); + }); + worker.on('message', function(value) { + assert.equal( + "worked", + value, + "test comunication" + ); + workerDone.resolve(); + }); + worker.postMessage("do it!") + } + }); + + let tab = yield new Promise(resolve => { + tabs.open({ + url: "about:", + onReady: resolve + }); + }); + assert.pass("opened tab"); + + yield workerDone.promise; + assert.pass("the worker has made a change"); + + let value = yield new Promise(resolve => { + mod.port.once("got-jep-107", resolve); + mod.port.emit("get-jep-107"); + }); + + assert.equal("worked", value, "attribute should be modified"); + + mod.destroy(); + assert.pass("the worker was destroyed"); +}; + +exports.testCommunication2 = function*(assert) { + let workerDone = defer(); + let url = data.url("test.html"); + + let mod = PageMod({ + include: url, + contentScriptWhen: 'start', + contentScript: 'new ' + function WorkerScope() { + document.documentElement.setAttribute('AUQLUE', 42); + + window.addEventListener('load', function listener() { + self.postMessage({ + msg: 'onload', + AUQLUE: document.documentElement.getAttribute('AUQLUE') + }); + }, false); + + self.on("message", function(msg) { + if (msg == "get window.test") { + unsafeWindow.changesInWindow(); + } + + self.postMessage({ + msg: document.documentElement.getAttribute("test") + }); + }); + }, + onAttach: function(worker) { + worker.on('error', function(e) { + assert.fail('Errors where reported'); + }); + worker.on('message', function({ msg, AUQLUE }) { + if ('onload' == msg) { + assert.equal('42', AUQLUE, 'PageMod scripts executed in order'); + worker.postMessage('get window.test'); + } + else { + assert.equal('changes in window', msg, 'PageMod test #2: second script has run'); + workerDone.resolve(); + } + }); + } + }); + + let tab = yield new Promise(resolve => { + tabs.open({ + url: url, + inBackground: true, + onReady: resolve + }); + }); + assert.pass("opened tab"); + + yield workerDone.promise; + + mod.destroy(); + assert.pass("the worker was destroyed"); +}; + +exports.testEventEmitter = function(assert, done) { + let workerDone = false, + callbackDone = null; + + testPageMod(assert, done, "about:", [{ + include: "about:*", + contentScript: 'new ' + function WorkerScope() { + self.port.on('addon-to-content', function(data) { + self.port.emit('content-to-addon', data); + }); + }, + onAttach: function(worker) { + worker.on('error', function(e) { + assert.fail('Errors were reported : '+e); + }); + worker.port.on('content-to-addon', function(value) { + assert.equal( + "worked", + value, + "EventEmitter API works!" + ); + if (callbackDone) + callbackDone(); + else + workerDone = true; + }); + worker.port.emit('addon-to-content', 'worked'); + } + }], + function(win, done) { + if (workerDone) + done(); + else + callbackDone = done; + } + ); +}; + +// Execute two concurrent page mods on same document to ensure that their +// JS contexts are different +exports.testMixedContext = function(assert, done) { + let doneCallback = null; + let messages = 0; + let modObject = { + include: "data:text/html;charset=utf-8,", + contentScript: 'new ' + function WorkerScope() { + // Both scripts will execute this, + // context is shared if one script see the other one modification. + let isContextShared = "sharedAttribute" in document; + self.postMessage(isContextShared); + document.sharedAttribute = true; + }, + onAttach: function(w) { + w.on("message", function (isContextShared) { + if (isContextShared) { + assert.fail("Page mod contexts are mixed."); + doneCallback(); + } + else if (++messages == 2) { + assert.pass("Page mod contexts are different."); + doneCallback(); + } + }); + } + }; + testPageMod(assert, done, "data:text/html;charset=utf-8,", [modObject, modObject], + function(win, done) { + doneCallback = done; + } + ); +}; + +exports.testHistory = function(assert, done) { + // We need a valid url in order to have a working History API. + // (i.e do not work on data: or about: pages) + // Test bug 679054. + let url = data.url("test-page-mod.html"); + let callbackDone = null; + testPageMod(assert, done, url, [{ + include: url, + contentScriptWhen: 'end', + contentScript: 'new ' + function WorkerScope() { + history.pushState({}, "", "#"); + history.replaceState({foo: "bar"}, "", "#"); + self.postMessage(history.state); + }, + onAttach: function(worker) { + worker.on('message', function (data) { + assert.equal(JSON.stringify(data), JSON.stringify({foo: "bar"}), + "History API works!"); + callbackDone(); + }); + } + }], + function(win, done) { + callbackDone = done; + } + ); +}; + +exports.testRelatedTab = function(assert, done) { + let tab; + let pageMod = new PageMod({ + include: "about:*", + onAttach: function(worker) { + assert.ok(!!worker.tab, "Worker.tab exists"); + assert.equal(tab, worker.tab, "Worker.tab is valid"); + pageMod.destroy(); + tab.close(done); + } + }); + + tabs.open({ + url: "about:", + onOpen: function onOpen(t) { + tab = t; + } + }); +}; + +// related to bug #989288 +// https://bugzilla.mozilla.org/show_bug.cgi?id=989288 +exports.testRelatedTabNewWindow = function(assert, done) { + let url = "about:logo" + let pageMod = new PageMod({ + include: url, + onAttach: function(worker) { + assert.equal(worker.tab.url, url, "Worker.tab.url is valid"); + worker.tab.close(done); + } + }); + + tabs.activeTab.attach({ + contentScript: "window.open('about:logo', '', " + + "'width=800,height=600,resizable=no,status=no,location=no');" + }); + +}; + +exports.testRelatedTabNoRequireTab = function(assert, done) { + let loader = Loader(module); + let tab; + let url = "data:text/html;charset=utf-8," + encodeURI("Test related worker tab 2"); + let { PageMod } = loader.require("sdk/page-mod"); + let pageMod = new PageMod({ + include: url, + onAttach: function(worker) { + assert.equal(worker.tab.url, url, "Worker.tab.url is valid"); + worker.tab.close(function() { + pageMod.destroy(); + loader.unload(); + done(); + }); + } + }); + + tabs.open(url); +}; + +exports.testRelatedTabNoOtherReqs = function(assert, done) { + let loader = Loader(module); + let { PageMod } = loader.require("sdk/page-mod"); + let pageMod = new PageMod({ + include: "about:blank?testRelatedTabNoOtherReqs", + onAttach: function(worker) { + assert.ok(!!worker.tab, "Worker.tab exists"); + pageMod.destroy(); + worker.tab.close(function() { + worker.destroy(); + loader.unload(); + done(); + }); + } + }); + + tabs.open({ + url: "about:blank?testRelatedTabNoOtherReqs" + }); +}; + +exports.testWorksWithExistingTabs = function(assert, done) { + let url = "data:text/html;charset=utf-8," + encodeURI("Test unique document"); + let { PageMod } = require("sdk/page-mod"); + tabs.open({ + url: url, + onReady: function onReady(tab) { + let pageModOnExisting = new PageMod({ + include: url, + attachTo: ["existing", "top", "frame"], + onAttach: function(worker) { + assert.ok(!!worker.tab, "Worker.tab exists"); + assert.equal(tab, worker.tab, "A worker has been created on this existing tab"); + + worker.on('pageshow', () => { + assert.fail("Should not have seen pageshow for an already loaded page"); + }); + + setTimeout(function() { + pageModOnExisting.destroy(); + pageModOffExisting.destroy(); + tab.close(done); + }, 0); + } + }); + + let pageModOffExisting = new PageMod({ + include: url, + onAttach: function(worker) { + assert.fail("pageModOffExisting page-mod should not have attached to anything"); + } + }); + } + }); +}; + +exports.testExistingFrameDoesntMatchInclude = function(assert, done) { + let iframeURL = 'data:text/html;charset=utf-8,UNIQUE-TEST-STRING-42'; + let iframe = '