mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-10 03:45:46 +00:00
Merge m-c to autoland, a=merge
MozReview-Commit-ID: 2YvHbITn9w3
This commit is contained in:
commit
cff9e9b197
@ -1,107 +0,0 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
# Integrates the xpcshell test runner with mach.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
|
||||
import mozpack.path as mozpath
|
||||
|
||||
from mozbuild.base import (
|
||||
MachCommandBase,
|
||||
)
|
||||
|
||||
from mach.decorators import (
|
||||
CommandArgument,
|
||||
CommandProvider,
|
||||
Command,
|
||||
)
|
||||
|
||||
@CommandProvider
|
||||
class MachCommands(MachCommandBase):
|
||||
@Command('generate-addon-sdk-moz-build', category='misc',
|
||||
description='Generates the moz.build file for the addon-sdk/ directory.')
|
||||
def run_addon_sdk_moz_build(self, **params):
|
||||
addon_sdk_dir = mozpath.join(self.topsrcdir, 'addon-sdk')
|
||||
js_src_dir = mozpath.join(addon_sdk_dir, 'source/lib')
|
||||
dirs_to_files = {}
|
||||
|
||||
for path, dirs, files in os.walk(js_src_dir):
|
||||
js_files = [f for f in files if f.endswith(('.js', '.jsm', '.html'))]
|
||||
if not js_files:
|
||||
continue
|
||||
|
||||
relative = mozpath.relpath(path, js_src_dir)
|
||||
dirs_to_files[relative] = js_files
|
||||
|
||||
moz_build = """# AUTOMATICALLY GENERATED FROM mozbuild.template AND mach. DO NOT EDIT.
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
%(moz-build-template)s
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk":
|
||||
%(non-b2g-modules)s
|
||||
%(always-on-modules)s"""
|
||||
|
||||
non_b2g_paths = [
|
||||
'method/test',
|
||||
'sdk/ui',
|
||||
'sdk/ui/button',
|
||||
'sdk/ui/sidebar',
|
||||
'sdk/places',
|
||||
'sdk/places/host',
|
||||
'sdk/tabs',
|
||||
'sdk/panel',
|
||||
'sdk/frame',
|
||||
'sdk/test',
|
||||
'sdk/window',
|
||||
'sdk/windows',
|
||||
'sdk/deprecated',
|
||||
]
|
||||
|
||||
non_b2g_modules = []
|
||||
always_on_modules = []
|
||||
|
||||
for d, files in sorted(dirs_to_files.items()):
|
||||
if d in non_b2g_paths:
|
||||
non_b2g_modules.append((d, files))
|
||||
else:
|
||||
always_on_modules.append((d, files))
|
||||
|
||||
def list_to_js_modules(l, indent=''):
|
||||
js_modules = []
|
||||
for d, files in l:
|
||||
if d == '':
|
||||
module_path = ''
|
||||
dir_path = ''
|
||||
else:
|
||||
# Ensure that we don't have things like:
|
||||
# EXTRA_JS_MODULES.commonjs.sdk.private-browsing
|
||||
# which would be a Python syntax error.
|
||||
path = d.split('/')
|
||||
module_path = ''.join('.' + p if p.find('-') == -1 else "['%s']" % p for p in path)
|
||||
dir_path = d + '/'
|
||||
filelist = ["'source/lib/%s%s'" % (dir_path, f)
|
||||
for f in sorted(files, key=lambda x: x.lower())]
|
||||
js_modules.append("EXTRA_JS_MODULES.commonjs%s += [\n %s,\n]\n"
|
||||
% (module_path, ',\n '.join(filelist)))
|
||||
stringified = '\n'.join(js_modules)
|
||||
# This isn't the same thing as |js_modules|, since |js_modules| had
|
||||
# embedded newlines.
|
||||
lines = stringified.split('\n')
|
||||
# Indent lines while avoiding trailing whitespace.
|
||||
lines = [indent + line if line else line for line in lines]
|
||||
return '\n'.join(lines)
|
||||
|
||||
moz_build_output = mozpath.join(addon_sdk_dir, 'moz.build')
|
||||
moz_build_template = mozpath.join(addon_sdk_dir, 'mozbuild.template')
|
||||
with open(moz_build_output, 'w') as f, open(moz_build_template, 'r') as t:
|
||||
substs = { 'moz-build-template': t.read(),
|
||||
'non-b2g-modules': list_to_js_modules(non_b2g_modules,
|
||||
indent=' '),
|
||||
'always-on-modules': list_to_js_modules(always_on_modules) }
|
||||
f.write(moz_build % substs)
|
@ -20,472 +20,103 @@ EXTRA_JS_MODULES.sdk.system += [
|
||||
'source/modules/system/Startup.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.method.test += [
|
||||
'source/lib/method/test/browser.js',
|
||||
'source/lib/method/test/common.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.deprecated += [
|
||||
'source/lib/sdk/deprecated/api-utils.js',
|
||||
'source/lib/sdk/deprecated/sync-worker.js',
|
||||
'source/lib/sdk/deprecated/unit-test-finder.js',
|
||||
'source/lib/sdk/deprecated/unit-test.js',
|
||||
'source/lib/sdk/deprecated/window-utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.frame += [
|
||||
'source/lib/sdk/frame/hidden-frame.js',
|
||||
'source/lib/sdk/frame/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.panel += [
|
||||
'source/lib/sdk/panel/events.js',
|
||||
'source/lib/sdk/panel/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.places += [
|
||||
'source/lib/sdk/places/bookmarks.js',
|
||||
'source/lib/sdk/places/contract.js',
|
||||
'source/lib/sdk/places/events.js',
|
||||
'source/lib/sdk/places/favicon.js',
|
||||
'source/lib/sdk/places/history.js',
|
||||
'source/lib/sdk/places/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.places.host += [
|
||||
'source/lib/sdk/places/host/host-bookmarks.js',
|
||||
'source/lib/sdk/places/host/host-query.js',
|
||||
'source/lib/sdk/places/host/host-tags.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.tabs += [
|
||||
'source/lib/sdk/tabs/common.js',
|
||||
'source/lib/sdk/tabs/events.js',
|
||||
'source/lib/sdk/tabs/helpers.js',
|
||||
'source/lib/sdk/tabs/namespace.js',
|
||||
'source/lib/sdk/tabs/observer.js',
|
||||
'source/lib/sdk/tabs/tab-fennec.js',
|
||||
'source/lib/sdk/tabs/tab-firefox.js',
|
||||
'source/lib/sdk/tabs/tab.js',
|
||||
'source/lib/sdk/tabs/tabs-firefox.js',
|
||||
'source/lib/sdk/tabs/utils.js',
|
||||
'source/lib/sdk/tabs/worker.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.test += [
|
||||
'source/lib/sdk/test/assert.js',
|
||||
'source/lib/sdk/test/harness.js',
|
||||
'source/lib/sdk/test/httpd.js',
|
||||
'source/lib/sdk/test/loader.js',
|
||||
'source/lib/sdk/test/memory.js',
|
||||
'source/lib/sdk/test/options.js',
|
||||
'source/lib/sdk/test/runner.js',
|
||||
'source/lib/sdk/test/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui += [
|
||||
'source/lib/sdk/ui/component.js',
|
||||
'source/lib/sdk/ui/frame.js',
|
||||
'source/lib/sdk/ui/id.js',
|
||||
'source/lib/sdk/ui/sidebar.js',
|
||||
'source/lib/sdk/ui/state.js',
|
||||
'source/lib/sdk/ui/toolbar.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.button += [
|
||||
'source/lib/sdk/ui/button/action.js',
|
||||
'source/lib/sdk/ui/button/contract.js',
|
||||
'source/lib/sdk/ui/button/toggle.js',
|
||||
'source/lib/sdk/ui/button/view.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.sidebar += [
|
||||
'source/lib/sdk/ui/sidebar/actions.js',
|
||||
'source/lib/sdk/ui/sidebar/contract.js',
|
||||
'source/lib/sdk/ui/sidebar/namespace.js',
|
||||
'source/lib/sdk/ui/sidebar/utils.js',
|
||||
'source/lib/sdk/ui/sidebar/view.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.window += [
|
||||
'source/lib/sdk/window/browser.js',
|
||||
'source/lib/sdk/window/events.js',
|
||||
'source/lib/sdk/window/helpers.js',
|
||||
'source/lib/sdk/window/namespace.js',
|
||||
'source/lib/sdk/window/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.windows += [
|
||||
'source/lib/sdk/windows/fennec.js',
|
||||
'source/lib/sdk/windows/firefox.js',
|
||||
'source/lib/sdk/windows/observer.js',
|
||||
'source/lib/sdk/windows/tabs-fennec.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs += [
|
||||
'source/lib/index.js',
|
||||
'source/lib/test.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk += [
|
||||
'source/lib/sdk/webextension.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.dev += [
|
||||
'source/lib/dev/debuggee.js',
|
||||
'source/lib/dev/frame-script.js',
|
||||
'source/lib/dev/panel.js',
|
||||
'source/lib/dev/ports.js',
|
||||
'source/lib/dev/theme.js',
|
||||
'source/lib/dev/toolbox.js',
|
||||
'source/lib/dev/utils.js',
|
||||
'source/lib/dev/volcan.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.dev.panel += [
|
||||
'source/lib/dev/panel/view.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.dev.theme += [
|
||||
'source/lib/dev/theme/hooks.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.diffpatcher += [
|
||||
'source/lib/diffpatcher/diff.js',
|
||||
'source/lib/diffpatcher/index.js',
|
||||
'source/lib/diffpatcher/patch.js',
|
||||
'source/lib/diffpatcher/rebase.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.diffpatcher.test += [
|
||||
'source/lib/diffpatcher/test/common.js',
|
||||
'source/lib/diffpatcher/test/diff.js',
|
||||
'source/lib/diffpatcher/test/index.js',
|
||||
'source/lib/diffpatcher/test/patch.js',
|
||||
'source/lib/diffpatcher/test/tap.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.framescript += [
|
||||
'source/lib/framescript/content.jsm',
|
||||
'source/lib/framescript/context-menu.js',
|
||||
'source/lib/framescript/FrameScriptManager.jsm',
|
||||
'source/lib/framescript/manager.js',
|
||||
'source/lib/framescript/util.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs['jetpack-id'] += [
|
||||
'source/lib/jetpack-id/index.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.method += [
|
||||
'source/lib/method/core.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs['mozilla-toolkit-versioning'] += [
|
||||
'source/lib/mozilla-toolkit-versioning/index.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs['mozilla-toolkit-versioning'].lib += [
|
||||
'source/lib/mozilla-toolkit-versioning/lib/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.node += [
|
||||
'source/lib/node/os.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk += [
|
||||
'source/lib/sdk/base64.js',
|
||||
'source/lib/sdk/clipboard.js',
|
||||
'source/lib/sdk/context-menu.js',
|
||||
'source/lib/sdk/context-menu@2.js',
|
||||
'source/lib/sdk/hotkeys.js',
|
||||
'source/lib/sdk/indexed-db.js',
|
||||
'source/lib/sdk/l10n.js',
|
||||
'source/lib/sdk/messaging.js',
|
||||
'source/lib/sdk/notifications.js',
|
||||
'source/lib/sdk/page-mod.js',
|
||||
'source/lib/sdk/page-worker.js',
|
||||
'source/lib/sdk/panel.js',
|
||||
'source/lib/sdk/passwords.js',
|
||||
'source/lib/sdk/private-browsing.js',
|
||||
'source/lib/sdk/querystring.js',
|
||||
'source/lib/sdk/request.js',
|
||||
'source/lib/sdk/selection.js',
|
||||
'source/lib/sdk/self.js',
|
||||
'source/lib/sdk/simple-prefs.js',
|
||||
'source/lib/sdk/simple-storage.js',
|
||||
'source/lib/sdk/system.js',
|
||||
'source/lib/sdk/tabs.js',
|
||||
'source/lib/sdk/test.js',
|
||||
'source/lib/sdk/timers.js',
|
||||
'source/lib/sdk/ui.js',
|
||||
'source/lib/sdk/url.js',
|
||||
'source/lib/sdk/windows.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.addon += [
|
||||
'source/lib/sdk/addon/bootstrap.js',
|
||||
'source/lib/sdk/addon/events.js',
|
||||
'source/lib/sdk/addon/host.js',
|
||||
'source/lib/sdk/addon/installer.js',
|
||||
'source/lib/sdk/addon/manager.js',
|
||||
'source/lib/sdk/addon/runner.js',
|
||||
'source/lib/sdk/addon/window.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.browser += [
|
||||
'source/lib/sdk/browser/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.console += [
|
||||
'source/lib/sdk/console/plain-text.js',
|
||||
'source/lib/sdk/console/traceback.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.content += [
|
||||
'source/lib/sdk/content/content-worker.js',
|
||||
'source/lib/sdk/content/content.js',
|
||||
'source/lib/sdk/content/context-menu.js',
|
||||
'source/lib/sdk/content/events.js',
|
||||
'source/lib/sdk/content/l10n-html.js',
|
||||
'source/lib/sdk/content/loader.js',
|
||||
'source/lib/sdk/content/mod.js',
|
||||
'source/lib/sdk/content/page-mod.js',
|
||||
'source/lib/sdk/content/page-worker.js',
|
||||
'source/lib/sdk/content/sandbox.js',
|
||||
'source/lib/sdk/content/tab-events.js',
|
||||
'source/lib/sdk/content/thumbnail.js',
|
||||
'source/lib/sdk/content/utils.js',
|
||||
'source/lib/sdk/content/worker-child.js',
|
||||
'source/lib/sdk/content/worker.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.content.sandbox += [
|
||||
'source/lib/sdk/content/sandbox/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk['context-menu'] += [
|
||||
'source/lib/sdk/context-menu/context.js',
|
||||
'source/lib/sdk/context-menu/core.js',
|
||||
'source/lib/sdk/context-menu/readers.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.core += [
|
||||
'source/lib/sdk/core/disposable.js',
|
||||
'source/lib/sdk/core/heritage.js',
|
||||
'source/lib/sdk/core/namespace.js',
|
||||
'source/lib/sdk/core/observer.js',
|
||||
'source/lib/sdk/core/promise.js',
|
||||
'source/lib/sdk/core/reference.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.deprecated.events += [
|
||||
'source/lib/sdk/deprecated/events/assembler.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.dom += [
|
||||
'source/lib/sdk/dom/events-shimmed.js',
|
||||
'source/lib/sdk/dom/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.dom.events += [
|
||||
'source/lib/sdk/dom/events/keys.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.event += [
|
||||
'source/lib/sdk/event/chrome.js',
|
||||
'source/lib/sdk/event/core.js',
|
||||
'source/lib/sdk/event/dom.js',
|
||||
'source/lib/sdk/event/target.js',
|
||||
'source/lib/sdk/event/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.fs += [
|
||||
'source/lib/sdk/fs/path.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.input += [
|
||||
'source/lib/sdk/input/browser.js',
|
||||
'source/lib/sdk/input/customizable-ui.js',
|
||||
'source/lib/sdk/input/frame.js',
|
||||
'source/lib/sdk/input/system.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.io += [
|
||||
'source/lib/sdk/io/buffer.js',
|
||||
'source/lib/sdk/io/byte-streams.js',
|
||||
'source/lib/sdk/io/file.js',
|
||||
'source/lib/sdk/io/fs.js',
|
||||
'source/lib/sdk/io/stream.js',
|
||||
'source/lib/sdk/io/text-streams.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.keyboard += [
|
||||
'source/lib/sdk/keyboard/hotkeys.js',
|
||||
'source/lib/sdk/keyboard/observer.js',
|
||||
'source/lib/sdk/keyboard/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.l10n += [
|
||||
'source/lib/sdk/l10n/core.js',
|
||||
'source/lib/sdk/l10n/html.js',
|
||||
'source/lib/sdk/l10n/loader.js',
|
||||
'source/lib/sdk/l10n/locale.js',
|
||||
'source/lib/sdk/l10n/plural-rules.js',
|
||||
'source/lib/sdk/l10n/prefs.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.l10n.json += [
|
||||
'source/lib/sdk/l10n/json/core.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.l10n.properties += [
|
||||
'source/lib/sdk/l10n/properties/core.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.lang += [
|
||||
'source/lib/sdk/lang/functional.js',
|
||||
'source/lib/sdk/lang/type.js',
|
||||
'source/lib/sdk/lang/weak-set.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.lang.functional += [
|
||||
'source/lib/sdk/lang/functional/concurrent.js',
|
||||
'source/lib/sdk/lang/functional/core.js',
|
||||
'source/lib/sdk/lang/functional/helpers.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.loader += [
|
||||
'source/lib/sdk/loader/cuddlefish.js',
|
||||
'source/lib/sdk/loader/sandbox.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.model += [
|
||||
'source/lib/sdk/model/core.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.net += [
|
||||
'source/lib/sdk/net/url.js',
|
||||
'source/lib/sdk/net/xhr.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.output += [
|
||||
'source/lib/sdk/output/system.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk['page-mod'] += [
|
||||
'source/lib/sdk/page-mod/match-pattern.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.passwords += [
|
||||
'source/lib/sdk/passwords/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.platform += [
|
||||
'source/lib/sdk/platform/xpcom.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.preferences += [
|
||||
'source/lib/sdk/preferences/event-target.js',
|
||||
'source/lib/sdk/preferences/native-options.js',
|
||||
'source/lib/sdk/preferences/service.js',
|
||||
'source/lib/sdk/preferences/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk['private-browsing'] += [
|
||||
'source/lib/sdk/private-browsing/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.remote += [
|
||||
'source/lib/sdk/remote/child.js',
|
||||
'source/lib/sdk/remote/core.js',
|
||||
'source/lib/sdk/remote/parent.js',
|
||||
'source/lib/sdk/remote/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.stylesheet += [
|
||||
'source/lib/sdk/stylesheet/style.js',
|
||||
'source/lib/sdk/stylesheet/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.system += [
|
||||
'source/lib/sdk/system/child_process.js',
|
||||
'source/lib/sdk/system/environment.js',
|
||||
'source/lib/sdk/system/events-shimmed.js',
|
||||
'source/lib/sdk/system/events.js',
|
||||
'source/lib/sdk/system/globals.js',
|
||||
'source/lib/sdk/system/process.js',
|
||||
'source/lib/sdk/system/runtime.js',
|
||||
'source/lib/sdk/system/unload.js',
|
||||
'source/lib/sdk/system/xul-app.js',
|
||||
'source/lib/sdk/system/xul-app.jsm',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.system.child_process += [
|
||||
'source/lib/sdk/system/child_process/subprocess.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.tab += [
|
||||
'source/lib/sdk/tab/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.button.view += [
|
||||
'source/lib/sdk/ui/button/view/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.frame += [
|
||||
'source/lib/sdk/ui/frame/model.js',
|
||||
'source/lib/sdk/ui/frame/view.html',
|
||||
'source/lib/sdk/ui/frame/view.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.state += [
|
||||
'source/lib/sdk/ui/state/events.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.ui.toolbar += [
|
||||
'source/lib/sdk/ui/toolbar/model.js',
|
||||
'source/lib/sdk/ui/toolbar/view.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.uri += [
|
||||
'source/lib/sdk/uri/resource.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.url += [
|
||||
'source/lib/sdk/url/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.util += [
|
||||
'source/lib/sdk/util/array.js',
|
||||
'source/lib/sdk/util/collection.js',
|
||||
'source/lib/sdk/util/contract.js',
|
||||
'source/lib/sdk/util/deprecate.js',
|
||||
'source/lib/sdk/util/dispatcher.js',
|
||||
'source/lib/sdk/util/list.js',
|
||||
'source/lib/sdk/util/match-pattern.js',
|
||||
'source/lib/sdk/util/object.js',
|
||||
'source/lib/sdk/util/rules.js',
|
||||
'source/lib/sdk/util/sequence.js',
|
||||
'source/lib/sdk/util/uuid.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.view += [
|
||||
'source/lib/sdk/view/core.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.worker += [
|
||||
'source/lib/sdk/worker/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.sdk.zip += [
|
||||
'source/lib/sdk/zip/utils.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.commonjs.toolkit += [
|
||||
'source/lib/toolkit/loader.js',
|
||||
'source/lib/toolkit/require.js',
|
||||
]
|
||||
modules = [
|
||||
'index.js',
|
||||
'jetpack-id/index.js',
|
||||
'method/core.js',
|
||||
'mozilla-toolkit-versioning/index.js',
|
||||
'mozilla-toolkit-versioning/lib/utils.js',
|
||||
'node/os.js',
|
||||
'sdk/addon/installer.js',
|
||||
'sdk/addon/window.js',
|
||||
'sdk/base64.js',
|
||||
'sdk/clipboard.js',
|
||||
'sdk/console/plain-text.js',
|
||||
'sdk/console/traceback.js',
|
||||
'sdk/core/disposable.js',
|
||||
'sdk/core/heritage.js',
|
||||
'sdk/core/namespace.js',
|
||||
'sdk/core/observer.js',
|
||||
'sdk/core/promise.js',
|
||||
'sdk/core/reference.js',
|
||||
'sdk/deprecated/unit-test-finder.js',
|
||||
'sdk/deprecated/unit-test.js',
|
||||
'sdk/deprecated/window-utils.js',
|
||||
'sdk/event/chrome.js',
|
||||
'sdk/event/core.js',
|
||||
'sdk/event/dom.js',
|
||||
'sdk/event/target.js',
|
||||
'sdk/event/utils.js',
|
||||
'sdk/frame/utils.js',
|
||||
'sdk/io/file.js',
|
||||
'sdk/lang/functional.js',
|
||||
'sdk/lang/functional/concurrent.js',
|
||||
'sdk/lang/functional/core.js',
|
||||
'sdk/lang/functional/helpers.js',
|
||||
'sdk/lang/type.js',
|
||||
'sdk/lang/weak-set.js',
|
||||
'sdk/net/url.js',
|
||||
'sdk/platform/xpcom.js',
|
||||
'sdk/preferences/service.js',
|
||||
'sdk/preferences/utils.js',
|
||||
'sdk/private-browsing.js',
|
||||
'sdk/private-browsing/utils.js',
|
||||
'sdk/querystring.js',
|
||||
'sdk/self.js',
|
||||
'sdk/system.js',
|
||||
'sdk/system/environment.js',
|
||||
'sdk/system/events.js',
|
||||
'sdk/system/globals.js',
|
||||
'sdk/system/process.js',
|
||||
'sdk/system/runtime.js',
|
||||
'sdk/system/unload.js',
|
||||
'sdk/system/xul-app.js',
|
||||
'sdk/system/xul-app.jsm',
|
||||
'sdk/test.js',
|
||||
'sdk/test/assert.js',
|
||||
'sdk/test/harness.js',
|
||||
'sdk/test/loader.js',
|
||||
'sdk/test/options.js',
|
||||
'sdk/test/utils.js',
|
||||
'sdk/timers.js',
|
||||
'sdk/uri/resource.js',
|
||||
'sdk/url.js',
|
||||
'sdk/url/utils.js',
|
||||
'sdk/util/array.js',
|
||||
'sdk/util/collection.js',
|
||||
'sdk/util/deprecate.js',
|
||||
'sdk/util/dispatcher.js',
|
||||
'sdk/util/list.js',
|
||||
'sdk/util/object.js',
|
||||
'sdk/util/sequence.js',
|
||||
'sdk/util/uuid.js',
|
||||
'sdk/window/utils.js',
|
||||
'sdk/zip/utils.js',
|
||||
'test.js',
|
||||
'toolkit/loader.js',
|
||||
'toolkit/require.js',
|
||||
]
|
||||
|
||||
commonjs = EXTRA_JS_MODULES.commonjs
|
||||
|
||||
sources = {}
|
||||
def get_sources(path):
|
||||
key = '/'.join(path)
|
||||
if key in sources:
|
||||
return sources[key]
|
||||
|
||||
source_dir = commonjs
|
||||
for dir_ in path:
|
||||
source_dir = source_dir[dir_]
|
||||
|
||||
sources[key] = source_dir
|
||||
return source_dir
|
||||
|
||||
for module in modules:
|
||||
path = module.split('/')[:-1]
|
||||
|
||||
source_dir = get_sources(path)
|
||||
source_dir += ['source/lib/%s' % module]
|
||||
|
||||
with Files("**"):
|
||||
BUG_COMPONENT = ("Add-on SDK", "General")
|
||||
|
@ -1,20 +0,0 @@
|
||||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=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/.
|
||||
|
||||
# Makefile.in uses a misc target through test_addons_TARGET.
|
||||
HAS_MISC_RULE = True
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
|
||||
JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini']
|
||||
JETPACK_ADDON_MANIFESTS += ['source/test/addons/jetpack-addon.ini']
|
||||
|
||||
EXTRA_JS_MODULES.sdk += [
|
||||
'source/app-extension/bootstrap.js',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.sdk.system += [
|
||||
'source/modules/system/Startup.js',
|
||||
]
|
@ -1,90 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Class } = require("../sdk/core/heritage");
|
||||
const { MessagePort, MessageChannel } = require("../sdk/messaging");
|
||||
const { DevToolsShim } = Cu.import("chrome://devtools-shim/content/DevToolsShim.jsm", {});
|
||||
|
||||
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.
|
||||
let transport = DevToolsShim.connectDebuggerServer();
|
||||
transports.set(this, transport);
|
||||
}
|
||||
// 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;
|
@ -1,120 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
(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");
|
||||
observerService.addObserver(observer, "content-document-loaded");
|
||||
observerService.addObserver(observer, "chrome-document-interactive");
|
||||
observerService.addObserver(observer, "chrome-document-loaded");
|
||||
addEventListener("unload", observer, false);
|
||||
|
||||
})(this);
|
@ -1,259 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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);
|
@ -1,14 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { method } = require("method/core");
|
||||
|
||||
const createView = method("dev/panel/view#createView");
|
||||
exports.createView = createView;
|
@ -1,64 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
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;
|
@ -1,135 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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;
|
@ -1,17 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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;
|
@ -1,107 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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 { DevToolsShim } = Cu.import("chrome://devtools-shim/content/DevToolsShim.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));
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
DevToolsShim.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);
|
||||
|
||||
DevToolsShim.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 => DevToolsShim.unregisterTool(Panel.prototype.id),
|
||||
values(this.panels));
|
||||
|
||||
each(Theme => DevToolsShim.unregisterTheme(Theme.prototype.id),
|
||||
values(this.themes));
|
||||
}
|
||||
});
|
||||
|
||||
validate.define(Tool, contract({
|
||||
panels: {
|
||||
is: ["object", "undefined"]
|
||||
},
|
||||
themes: {
|
||||
is: ["object", "undefined"]
|
||||
}
|
||||
}));
|
||||
|
||||
exports.Tool = Tool;
|
@ -1,39 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { DevToolsShim } = Cu.import("chrome://devtools-shim/content/DevToolsShim.jsm", {});
|
||||
|
||||
const { getActiveTab } = require("../sdk/tabs/utils");
|
||||
const { getMostRecentBrowserWindow } = require("../sdk/window/utils");
|
||||
|
||||
const targetFor = target => {
|
||||
target = target || getActiveTab(getMostRecentBrowserWindow());
|
||||
return DevToolsShim.getTargetForTab(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 DevToolsShim.showToolbox(targetFor(tab), id);
|
||||
};
|
||||
exports.openToolbox = openToolbox;
|
||||
|
||||
const closeToolbox = tab => DevToolsShim.closeToolbox(targetFor(tab));
|
||||
exports.closeToolbox = closeToolbox;
|
||||
|
||||
const getToolbox = tab => DevToolsShim.getToolbox(targetFor(tab));
|
||||
exports.getToolbox = getToolbox;
|
||||
|
||||
const openToolboxPanel = (id, tab) => {
|
||||
id = getId(id);
|
||||
return DevToolsShim.showToolbox(targetFor(tab), id).then(getCurrentPanel);
|
||||
};
|
||||
exports.openToolboxPanel = openToolboxPanel;
|
File diff suppressed because one or more lines are too long
@ -1,45 +0,0 @@
|
||||
"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
|
@ -1,5 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
exports.diff = require("./diff")
|
||||
exports.patch = require("./patch")
|
||||
exports.rebase = require("./rebase")
|
@ -1,21 +0,0 @@
|
||||
"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
|
@ -1,36 +0,0 @@
|
||||
"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
|
@ -1,3 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
require("test").run(require("./index"))
|
@ -1,59 +0,0 @@
|
||||
"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")
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
"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")
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
"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")
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
require("retape")(require("./index"))
|
@ -1,27 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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'];
|
@ -1,94 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,215 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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.ownerGlobal.HTMLVideoElement &&
|
||||
!isVideoLoadingAudio(node);
|
||||
|
||||
const isAudio = node => {
|
||||
const {HTMLVideoElement, HTMLAudioElement} = node.ownerGlobal;
|
||||
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.ownerGlobal.getSelection().isCollapsed &&
|
||||
!node.matches(nonPageSelector));
|
||||
|
||||
// Reads `true` if node is in an iframe otherwise returns true.
|
||||
parsers["reader/isFrame()"] = constant(node =>
|
||||
!!node.ownerGlobal.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;
|
@ -1,26 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const 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;
|
@ -1,25 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
|
||||
const { 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.ownerGlobal);
|
||||
exports.nodeToMessageManager = nodeToMessageManager;
|
@ -1,20 +0,0 @@
|
||||
"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)
|
@ -1,272 +0,0 @@
|
||||
"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)
|
183
addon-sdk/source/lib/sdk/addon/bootstrap.js
vendored
183
addon-sdk/source/lib/sdk/addon/bootstrap.js
vendored
@ -1,183 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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.
|
||||
if (addon.webExtension)
|
||||
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;
|
@ -1,56 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
module.metadata = {
|
||||
'stability': '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 || {}));
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
exports.request = {};
|
||||
exports.response = {};
|
@ -1,18 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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;
|
@ -1,176 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
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 { exit, env, staticArgs } = require('../system');
|
||||
const { when: unload } = require('../system/unload');
|
||||
const globals = require('../system/globals');
|
||||
const { get } = require('../preferences/service');
|
||||
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, "DevToolsShim", function () {
|
||||
return Cu.import("chrome://devtools-shim/content/DevToolsShim.jsm", {}).
|
||||
DevToolsShim;
|
||||
});
|
||||
|
||||
// Initializes default preferences
|
||||
function setDefaultPrefs(prefsURI) {
|
||||
const prefs = Cc['@mozilla.org/preferences-service;1'].
|
||||
getService(Ci.nsIPrefService).
|
||||
QueryInterface(Ci.nsIPrefBranch);
|
||||
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).
|
||||
catch(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);
|
||||
}).catch(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)) {
|
||||
DevToolsShim.initBrowserToolboxProcessForAddon(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);
|
||||
});
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { 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));
|
@ -16,7 +16,6 @@ module.metadata = {
|
||||
|
||||
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.
|
||||
@ -90,15 +89,6 @@ exports.set = function(aData, aDataType) {
|
||||
}
|
||||
}
|
||||
|
||||
options = apiUtils.validateOptions(options, {
|
||||
data: {
|
||||
is: ["string"]
|
||||
},
|
||||
datatype: {
|
||||
is: ["string"]
|
||||
}
|
||||
});
|
||||
|
||||
let flavor = fromJetpackFlavor(options.datatype);
|
||||
|
||||
if (!flavor)
|
||||
@ -208,12 +198,6 @@ exports.get = function(aDataType) {
|
||||
options.datatype = "text";
|
||||
}
|
||||
|
||||
options = apiUtils.validateOptions(options, {
|
||||
datatype: {
|
||||
is: ["string"]
|
||||
}
|
||||
});
|
||||
|
||||
var xferable = Cc["@mozilla.org/widget/transferable;1"].
|
||||
createInstance(Ci.nsITransferable);
|
||||
if (!xferable)
|
||||
|
@ -1,305 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
@ -1,17 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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;
|
||||
}
|
||||
});
|
@ -1,407 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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.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.ownerGlobal.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.ownerGlobal.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.ownerGlobal;
|
||||
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.ownerGlobal);
|
||||
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.ownerGlobal);
|
||||
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);
|
||||
}
|
||||
});
|
@ -1,57 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Ci } = require("chrome");
|
||||
lazyRequire(this, "../event/dom", "open");
|
||||
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]);
|
@ -1,132 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Ci, Cc, Cu } = require("chrome");
|
||||
lazyRequireModule(this, "../l10n/core", "core");
|
||||
lazyRequire(this, "../stylesheet/utils", "loadSheet", "removeSheet");
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
@ -1,74 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { 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);
|
@ -1,230 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "stable"
|
||||
};
|
||||
|
||||
lazyRequire(this, '../content/utils', 'getAttachEventType');
|
||||
const { Class } = require('../core/heritage');
|
||||
const { Disposable } = require('../core/disposable');
|
||||
lazyRequire(this, './worker-child', 'WorkerChild');
|
||||
const { EventTarget } = require('../event/target');
|
||||
const { on, emit, once, setListeners } = require('../event/core');
|
||||
lazyRequire(this, '../dom/events',{'on': 'domOn', 'removeListener': 'domOff'});
|
||||
lazyRequire(this, '../util/object', "merge");
|
||||
lazyRequire(this, '../window/utils', "getFrames");
|
||||
lazyRequire(this, '../private-browsing/utils', "ignoreWindow");
|
||||
lazyRequire(this, '../stylesheet/style', 'Style');
|
||||
lazyRequire(this, '../content/mod', 'attach', 'detach');
|
||||
lazyRequire(this, '../util/rules', 'Rules');
|
||||
lazyRequire(this, '../util/uuid', '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 (this.attachTo.includes('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 (mod.attachTo.includes("top") && modMatchesURI(mod, uri))
|
||||
onContent(mod, window);
|
||||
if (mod.attachTo.includes("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 && !mod.attachTo.includes("top"))
|
||||
return;
|
||||
// Is a frame document and `frame` is not set, ignore
|
||||
if (!isTopDocument && !mod.attachTo.includes("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();
|
||||
});
|
@ -1,157 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { frames } = require("../remote/child");
|
||||
const { Class } = require("../core/heritage");
|
||||
const { Disposable } = require('../core/disposable');
|
||||
lazyRequire(this, "../self", "data");
|
||||
lazyRequire(this, "../dom/events", "once");
|
||||
lazyRequire(this, "./utils", "getAttachEventType");
|
||||
lazyRequire(this, '../util/rules', "Rules");
|
||||
lazyRequire(this, '../util/uuid', "uuid");
|
||||
lazyRequire(this, "./worker-child", "WorkerChild");
|
||||
const { Cc, Ci, Cu } = require("chrome");
|
||||
const { on: onSystemEvent } = require("../system/events");
|
||||
|
||||
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, 'appShell',
|
||||
"@mozilla.org/appshell/appShellService;1",
|
||||
"nsIAppShellService");
|
||||
|
||||
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"])
|
||||
});
|
||||
|
||||
onSystemEvent(DOC_INSERTED, ({type, subject, data}) => {
|
||||
let page = Array.from(pages.values()).find(p => p.contentWindow.document === subject);
|
||||
|
||||
if (!page)
|
||||
return;
|
||||
|
||||
if (getAttachEventType(page.options) == DOC_INSERTED)
|
||||
page.attachWorker();
|
||||
}, true);
|
||||
|
||||
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();
|
||||
});
|
@ -1,426 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
'use strict';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
const { Class } = require('../core/heritage');
|
||||
const { EventTarget } = require('../event/target');
|
||||
lazyRequire(this, '../event/core', "on", "off", "emit");
|
||||
lazyRequire(this, './sandbox/events', "events");
|
||||
lazyRequire(this, './utils', "requiresAddonGlobal");
|
||||
lazyRequire(this, '../lang/functional', {"delay": "async"});
|
||||
const { Ci, Cu, Cc } = require('chrome');
|
||||
lazyRequireModule(this, "../timers", "timer");
|
||||
lazyRequire(this, '../url', "URL");
|
||||
lazyRequire(this, '../loader/sandbox', "sandbox", "evaluate", "load");
|
||||
lazyRequire(this, '../util/object', "merge");
|
||||
lazyRequire(this, '../tabs/utils', "getTabForContentWindowNoShim");
|
||||
lazyRequire(this, '../window/utils', "getInnerId");
|
||||
lazyRequire(this, '../console/plain-text', "PlainTextConsole");
|
||||
|
||||
lazyRequire(this, '../self', "data");
|
||||
lazyRequire(this, '../remote/core', "isChildLoader");
|
||||
|
||||
// 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'),
|
||||
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});
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const events = {};
|
||||
exports.events = events;
|
@ -1,58 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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 });
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
'use strict';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
const { Cc, Ci, 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';
|
@ -1,158 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
'use strict';
|
||||
|
||||
lazyRequire(this, '../util/object', 'merge');
|
||||
const { Class } = require('../core/heritage');
|
||||
lazyRequire(this, '../event/core', 'emit');
|
||||
const { EventTarget } = require('../event/target');
|
||||
lazyRequire(this, '../window/utils', 'getInnerId');
|
||||
lazyRequire(this, '../lang/type', 'instanceOf', 'isObject');
|
||||
lazyRequireModule(this, '../system/events', 'system');
|
||||
const { when } = require('../system/unload');
|
||||
lazyRequire(this, './sandbox', 'WorkerSandbox');
|
||||
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);
|
||||
});
|
@ -1,180 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
lazyRequire(this, '../event/core', "emit");
|
||||
const { omit, merge } = require('../util/object');
|
||||
const { Class } = require('../core/heritage');
|
||||
const { method } = require('../lang/functional');
|
||||
lazyRequire(this, '../window/utils', "getInnerId");
|
||||
const { EventTarget } = require('../event/target');
|
||||
lazyRequire(this, '../private-browsing/utils', "isPrivate");
|
||||
lazyRequire(this, '../tabs/utils', "getTabForBrowser", "getTabForContentWindowNoShim", "getBrowserForTab");
|
||||
lazyRequire(this, './utils', "attach", "connect", "detach", "destroy", "makeChildOptions");
|
||||
const { ensure } = require('../system/unload');
|
||||
lazyRequire(this, '../system/events', {"on": "observe"});
|
||||
const { Ci, Cu } = require('chrome');
|
||||
lazyRequire(this, 'sdk/model/core', {"modelFor": "tabFor"});
|
||||
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);
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -1,146 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { Class } = require("../core/heritage");
|
||||
lazyRequire(this, "../util/match-pattern", "MatchPattern");
|
||||
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;
|
@ -1,384 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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);
|
||||
},
|
||||
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);
|
@ -1,112 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
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;
|
@ -1,32 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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);
|
@ -1,197 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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;
|
@ -1,54 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
@ -1,288 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
*
|
||||
* `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));
|
||||
}
|
@ -34,7 +34,6 @@ const findAndRunTests = function findAndRunTests(options) {
|
||||
exports.findAndRunTests = findAndRunTests;
|
||||
|
||||
var runnerWindows = new WeakMap();
|
||||
var runnerTabs = new WeakMap();
|
||||
|
||||
const TestRunner = function TestRunner(options) {
|
||||
options = options || {};
|
||||
@ -42,7 +41,6 @@ const TestRunner = function TestRunner(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;
|
||||
@ -330,31 +328,18 @@ TestRunner.prototype = {
|
||||
|
||||
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);
|
||||
tabs = [];
|
||||
console.log(win.location + " - " + tabs.map(getURI).join(", "));
|
||||
}
|
||||
else {
|
||||
|
@ -1,18 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
module.metadata = {
|
||||
'stability': 'unstable'
|
||||
};
|
||||
|
||||
const 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);
|
@ -1,192 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { 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.ownerGlobal;
|
||||
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;
|
@ -1,63 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { emit } = require("../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));
|
||||
};
|
||||
|
@ -1,115 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const { Class } = require("../core/heritage");
|
||||
const { List, addListItem, removeListItem } = require("../util/list");
|
||||
const { EventTarget } = require("../event/target");
|
||||
lazyRequire(this, "../event/core", "emit");
|
||||
lazyRequire(this, "./utils", { "create": "makeFrame" });
|
||||
lazyRequire(this, "../core/promise", "defer");
|
||||
const { when: unload } = require("../system/unload");
|
||||
lazyRequire(this, "../deprecated/api-utils", "validateOptions", "getTypeOf");
|
||||
lazyRequire(this, "../addon/window", "window");
|
||||
lazyRequire(this, "../util/array", "fromIterator");
|
||||
|
||||
// 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);
|
||||
deferred.resolve(target);
|
||||
}
|
||||
});
|
||||
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.remove()
|
||||
}
|
||||
exports.remove = removeHiddenFrame;
|
||||
|
||||
unload(() => fromIterator(cache).forEach(removeHiddenFrame));
|
@ -1,500 +0,0 @@
|
||||
// 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;
|
||||
};
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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);
|
||||
};
|
@ -1,78 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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);
|
||||
|
||||
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;
|
@ -1,73 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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;
|
@ -1,28 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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;
|
@ -1,85 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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;
|
@ -1,113 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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;
|
@ -1,351 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
module.metadata = {
|
||||
'stability': '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');
|
||||
lazyRequire(this, 'sdk/lang/type', "isNumber");
|
||||
Cu.importGlobalProperties(["TextEncoder", "TextDecoder"]);
|
||||
|
||||
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 (/invalid array length/.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);
|
||||
}
|
||||
});
|
||||
});
|
@ -1,104 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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;
|
||||
}
|
||||
};
|
@ -1,985 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { Cc, Ci, CC } = require("chrome");
|
||||
|
||||
lazyRequire(this, "../timers", "setTimeout");
|
||||
lazyRequire(this, "./stream", "Stream", "InputStream", "OutputStream");
|
||||
lazyRequire(this, "../event/core", "emit", "on");
|
||||
lazyRequire(this, "./buffer", "Buffer");
|
||||
|
||||
const { ns } = require("../core/namespace");
|
||||
const { Class } = require("../core/heritage");
|
||||
|
||||
|
||||
const LocalFile = CC("@mozilla.org/file/local;1", "nsIFile",
|
||||
"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 LocalFile(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 LocalFile(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 LocalFile(oldPath);
|
||||
let target = new LocalFile(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 LocalFile(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 LocalFile(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 LocalFile(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 LocalFile(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 LocalFile(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 LocalFile(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), LocalFile(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;
|
@ -1,441 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
const { CC, Cc, Ci, Cu, Cr, components } = require("chrome");
|
||||
const { EventTarget } = require("../event/target");
|
||||
const { Class } = require("../core/heritage");
|
||||
|
||||
lazyRequire(this, "../event/core", "emit");
|
||||
lazyRequire(this, "./buffer", "Buffer");
|
||||
lazyRequire(this, "../timers", "setTimeout");
|
||||
|
||||
|
||||
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;
|
@ -1,235 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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;
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { 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();
|
||||
}
|
||||
}
|
||||
});
|
@ -1,57 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { 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.ownerGlobal || undefined);
|
||||
}
|
||||
});
|
||||
|
||||
exports.observer = new Observer();
|
@ -1,189 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
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);
|
||||
};
|
@ -1,94 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "stable"
|
||||
};
|
||||
|
||||
lazyRequireModule(this, "./l10n/json/core", "json");
|
||||
lazyRequire(this, "./l10n/core", {"get": "getKey"});
|
||||
lazyRequireModule(this, "./l10n/properties/core", "properties");
|
||||
lazyRequire(this, "./l10n/plural-rules", "getRulesForLocale");
|
||||
|
||||
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
// Retrieve the plural mapping function
|
||||
XPCOMUtils.defineLazyGetter(this, "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;
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
lazyRequireModule(this, "./json/core", "json");
|
||||
lazyRequireModule(this, "./properties/core", "properties");
|
||||
|
||||
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "get",
|
||||
() => json.usingJSON ? json.get : properties.get);
|
||||
|
||||
module.exports = Object.freeze({
|
||||
get get() { return get; }, // ... yeah.
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { 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");
|
||||
});
|
@ -1,36 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
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";
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
lazyRequire(this, "./locale", "getPreferedLocales", "findClosestLocale");
|
||||
lazyRequire(this, "../net/url", "readURI");
|
||||
lazyRequire(this, "../core/promise", "resolve");
|
||||
|
||||
function parseJsonURI(uri) {
|
||||
return readURI(uri).
|
||||
then(JSON.parse).
|
||||
catch(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
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const prefs = require("../preferences/service");
|
||||
const { Cu, Cc, Ci } = require("chrome");
|
||||
const { Services } = Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
function getPreferedLocales(caseSensitve) {
|
||||
const locales = Services.locale.getRequestedLocales();
|
||||
|
||||
// This API expects to always append en-US fallback, so for compatibility
|
||||
// reasons, we're going to inject it here.
|
||||
// See bug 1373061 for details.
|
||||
if (!locales.includes('en-US')) {
|
||||
locales.push('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;
|
||||
}
|
@ -1,407 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// 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];
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
lazyRequire(this, "../system/events", "on");
|
||||
lazyRequireModule(this, "./core", "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;
|
@ -1,89 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
lazyRequire(this, '../../url/utils', 'newURI');
|
||||
lazyRequire(this, "../plural-rules", 'getRulesForLocale');
|
||||
lazyRequire(this, '../locale', 'getPreferedLocales');
|
||||
const { rootURI } = require("@loader/options");
|
||||
const { Services } = require("resource://gre/modules/Services.jsm");
|
||||
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
const baseURI = rootURI + "locale/";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "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));
|
@ -1,102 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
// 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
|
||||
});
|
@ -1,65 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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');
|
||||
|
||||
/**
|
||||
* 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);
|
||||
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.loadSubScriptWithOptions(uri, {target: sandbox,
|
||||
charset: 'UTF-8',
|
||||
wantReturnValue: true});
|
||||
}
|
||||
}
|
||||
exports.load = load;
|
||||
|
||||
/**
|
||||
* Forces the given `sandbox` to be freed immediately.
|
||||
*/
|
||||
exports.nuke = Cu.nukeSandbox
|
@ -1,12 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { window } = require("sdk/addon/window");
|
||||
exports.MessageChannel = window.MessageChannel;
|
||||
exports.MessagePort = window.MessagePort;
|
@ -1,23 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { 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;
|
@ -1,37 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "stable"
|
||||
};
|
||||
|
||||
const { deprecateFunction } = require("../util/deprecate");
|
||||
const { Ci, Cu } = require("chrome");
|
||||
|
||||
Cu.importGlobalProperties(["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.
|
@ -1,112 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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"]
|
||||
}
|
||||
});
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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;
|
@ -1,190 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "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');
|
||||
lazyRequire(this, './content/worker', "Worker");
|
||||
const { EventTarget } = require('./event/target');
|
||||
lazyRequire(this, './event/core', "on", "emit", "once", "setListeners");
|
||||
lazyRequire(this, './lang/type', "isRegExp", "isUndefined");
|
||||
const { merge, omit } = require('./util/object');
|
||||
lazyRequire(this, "./util/array", "remove", "has", "hasAny");
|
||||
lazyRequire(this, "./util/rules", "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);
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
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");
|
@ -1,194 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "stable"
|
||||
};
|
||||
|
||||
const { Class } = require('./core/heritage');
|
||||
const { ns } = require('./core/namespace');
|
||||
lazyRequire(this, './event/utils', "pipe", "stripListeners");
|
||||
const { connect, destroy, WorkerHost } = require('./content/utils');
|
||||
lazyRequire(this, './content/worker', "Worker");
|
||||
const { Disposable } = require('./core/disposable');
|
||||
const { EventTarget } = require('./event/target');
|
||||
lazyRequire(this, './event/core', "setListeners");
|
||||
lazyRequire(this, './addon/window', "window");
|
||||
lazyRequire(this, './frame/utils', { "create": "makeFrame" }, "getDocShell");
|
||||
const { contract } = require('./util/contract');
|
||||
const { contract: loaderContract } = require('./content/loader');
|
||||
lazyRequire(this, './util/rules', "Rules");
|
||||
const { merge } = require('./util/object');
|
||||
lazyRequire(this, './util/uuid', "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);
|
||||
});
|
@ -1,436 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
// The panel module currently supports only Firefox 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");
|
||||
lazyRequire(this, './timers', "setTimeout");
|
||||
const { Class } = require("./core/heritage");
|
||||
const { DefaultWeakMap, merge } = require("./util/object");
|
||||
const { WorkerHost } = require("./content/utils");
|
||||
lazyRequire(this, "./deprecated/sync-worker", "Worker");
|
||||
const { Disposable } = require("./core/disposable");
|
||||
const { WeakReference } = require('./core/reference');
|
||||
const { contract: loaderContract } = require("./content/loader");
|
||||
const { contract } = require("./util/contract");
|
||||
lazyRequire(this, "./event/core", "on", "off", "emit", "setListeners");
|
||||
const { EventTarget } = require("./event/target");
|
||||
lazyRequireModule(this, "./panel/utils", "domPanel");
|
||||
lazyRequire(this, './frame/utils', "getDocShell");
|
||||
const { events } = require("./panel/events");
|
||||
const { filter, pipe, stripListeners } = require("./event/utils");
|
||||
lazyRequire(this, "./view/core", "getNodeView", "getActiveView");
|
||||
lazyRequire(this, "./lang/type", "isNil", "isObject", "isNumber");
|
||||
lazyRequire(this, "./content/utils", "getAttachEventType");
|
||||
const { number, boolean, object } = require('./deprecated/api-utils');
|
||||
lazyRequire(this, "./stylesheet/style", "Style");
|
||||
lazyRequire(this, "./content/mod", "attach", "detach");
|
||||
|
||||
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 optionsMap = new WeakMap();
|
||||
var panels = new WeakMap();
|
||||
var models = new WeakMap();
|
||||
var views = new DefaultWeakMap(panel => {
|
||||
let model = models.get(panel);
|
||||
|
||||
// Setup view
|
||||
let viewOptions = {allowJavascript: !model.allow || (model.allow.script !== false)};
|
||||
let view = domPanel.make(null, viewOptions);
|
||||
panels.set(view, panel);
|
||||
|
||||
// Load panel content.
|
||||
domPanel.setURL(view, model.contentURL);
|
||||
|
||||
// Allow context menu
|
||||
domPanel.allowContextMenu(view, model.contextMenu);
|
||||
|
||||
return view;
|
||||
});
|
||||
var workers = new DefaultWeakMap(panel => {
|
||||
let options = optionsMap.get(panel);
|
||||
|
||||
let worker = new Worker(stripListeners(options));
|
||||
workers.set(panel, worker);
|
||||
|
||||
// pipe events from worker to a panel.
|
||||
pipe(worker, panel);
|
||||
|
||||
return worker;
|
||||
});
|
||||
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);
|
||||
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);
|
||||
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
|
||||
}));
|
||||
}
|
||||
|
||||
optionsMap.set(this, options);
|
||||
|
||||
// Setup listeners.
|
||||
setListeners(this, options);
|
||||
},
|
||||
dispose: function dispose() {
|
||||
if (views.has(this))
|
||||
this.hide();
|
||||
off(this);
|
||||
|
||||
workerFor(this).destroy();
|
||||
detach(styleFor(this));
|
||||
|
||||
if (views.has(this))
|
||||
domPanel.dispose(viewFor(this));
|
||||
|
||||
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) {
|
||||
let view = viewFor(this);
|
||||
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 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);
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// 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");
|
||||
lazyRequire(this, "../event/core", "emit");
|
||||
|
||||
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;
|
@ -1,451 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
module.metadata = {
|
||||
"stability": "unstable"
|
||||
};
|
||||
|
||||
const { Cc, Ci } = require("chrome");
|
||||
const { Services } = require("resource://gre/modules/Services.jsm");
|
||||
lazyRequire(this, "../timers", "setTimeout");
|
||||
lazyRequire(this, "../system", "platform");
|
||||
lazyRequire(this, "../window/utils", "getMostRecentBrowserWindow", "getOwnerBrowserWindow",
|
||||
"getScreenPixelsPerCSSPixel");
|
||||
|
||||
lazyRequire(this, "../frame/utils", { "create": "createFrame" }, "swapFrameLoaders", "getDocShell");
|
||||
lazyRequire(this, "../addon/window", { "window": "addonWindow" });
|
||||
lazyRequire(this, "../lang/type", "isNil");
|
||||
lazyRequire(this, '../self', "data");
|
||||
|
||||
lazyRequireModule(this, "../system/events", "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.ownerGlobal;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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.remove();
|
||||
}
|
||||
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;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user