From c205181a5311733efd861a49d4cf539a997220e3 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 24 Aug 2014 20:53:44 +0100 Subject: [PATCH 001/105] Add sdk/ prefix to Firefox/lib/main.js require()s --- Firefox/lib/main.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Firefox/lib/main.js b/Firefox/lib/main.js index 6d09acc..d510032 100644 --- a/Firefox/lib/main.js +++ b/Firefox/lib/main.js @@ -1,14 +1,14 @@ // Import the APIs we need. -var pageMod = require("page-mod"); -var Request = require("request").Request; -var notifications = require("notifications"); -var self = require("self"); -var tabs = require("tabs"); -var ss = require("simple-storage"); +var pageMod = require("sdk/page-mod"); +var Request = require("sdk/request").Request; +var notifications = require("sdk/notifications"); +var self = require("sdk/self"); +var tabs = require("sdk/tabs"); +var ss = require("sdk/simple-storage"); var workers = []; -var contextMenu = require("context-menu"); -var priv = require("private-browsing"); +var contextMenu = require("sdk/context-menu"); +var priv = require("sdk/private-browsing"); var windows = require("sdk/windows").browserWindows; // require chrome allows us to use XPCOM objects... From d68334b88bda06dfae953929a8a18666465ba68d Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 25 Aug 2014 01:47:26 +0100 Subject: [PATCH 002/105] Changes for recent versions of Jetpack --- Firefox/README.md | 1 - Firefox/package.json | 2 +- Firefox/test/test-main.js | 12 ++++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) delete mode 100644 Firefox/README.md create mode 100644 Firefox/test/test-main.js diff --git a/Firefox/README.md b/Firefox/README.md deleted file mode 100644 index 49a92fc..0000000 --- a/Firefox/README.md +++ /dev/null @@ -1 +0,0 @@ -The Firefox Addon SDK requires you have a README.md present. \ No newline at end of file diff --git a/Firefox/package.json b/Firefox/package.json index 3379d5d..d03a861 100644 --- a/Firefox/package.json +++ b/Firefox/package.json @@ -3,7 +3,7 @@ "license": "GPL", "author": "honestbleeps", "version": "0.95", - "fullName": "BabelExt", + "title": "BabelExt", "id": "jid1-h7LuK9FSeAYouw", "name": "babelext_your_name_here" } diff --git a/Firefox/test/test-main.js b/Firefox/test/test-main.js new file mode 100644 index 0000000..147f98a --- /dev/null +++ b/Firefox/test/test-main.js @@ -0,0 +1,12 @@ +var main = require("./main"); + +exports["test main"] = function(assert) { + assert.pass("Unit test running!"); +}; + +exports["test main async"] = function(assert, done) { + assert.pass("async Unit test running!"); + done(); +}; + +require("sdk/test").run(exports); From 1da4ce94c6bcf332fcd2031006179176f98a8b63 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 24 Aug 2014 16:55:37 +0100 Subject: [PATCH 003/105] Reimplement makelinks.sh as a wrapper around makelinks.bat makelinks.bat had several issues (bashisms with #!/bin/sh, incorrect handling of .user.js files etc.) Rather than keep a second version in sync with the .bat file that seems better-maintained, this commit rewrites the whole thing to read the .bat and mimic its behaviour. --- makelinks.sh | 47 +++++++++++++++-------------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/makelinks.sh b/makelinks.sh index 6b2a69f..7858450 100755 --- a/makelinks.sh +++ b/makelinks.sh @@ -1,34 +1,17 @@ -#!/bin/sh -files=("BabelExt.js" "extension.js") -paths=("Chrome" "Firefox/data" "Opera" "Safari.safariextension") +#!/bin/bash -for i in "${files[@]}" +while read -r MKLINK H TO FROM do - for j in "${paths[@]}" - do - if [ "$j" == "Opera" ]; - then - if [[ "$i" == *.user.js || "$i" == *.css ]]; - then - dest="./$j/includes/" - else - dest="./$j/modules/" - fi - else - dest="./$j/" - fi - echo "Re-linking:" $dest$i - if [ -f $dest$i ]; - then - rm $dest$i - fi - - if [ "clean" != "$1" ]; - then - mkdir -p $dest - ln ./lib/$i $dest - fi - done -done - - + if [ "$MKLINK" == "mklink" ] + then + SYMLINK="" + if [ "$H" != "/H" ] + then + SYMLINK="-s" + FROM="$TO" + TO="$H" + fi + rm -f "${TO//\\//}" + ln $SYMLINK "${FROM//\\//}" "${TO//\\//}" + fi +done < makelinks.bat From d48d13d5a859fe55d15ae06076d635104b537751 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 24 Aug 2014 18:57:08 +0100 Subject: [PATCH 004/105] Add Makefile and browser-neutral configuration Note: this replaces makelinks.sh --- .gitignore | 5 + Chrome/.gitignore | 3 +- Chrome/manifest.json | 15 +- Firefox/data/.gitignore | 3 +- Makefile | 223 ++++++++++++++++++++++++++++++ Opera/includes/.gitignore | 3 +- Safari.safariextension/.gitignore | 3 +- lib/config.txt | 35 +++++ makelinks.sh | 17 --- 9 files changed, 273 insertions(+), 34 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 lib/config.txt delete mode 100755 makelinks.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..972eba3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/firefox-unpacked +/firefox-addon-sdk +/Chrome.pem +/build +*~ diff --git a/Chrome/.gitignore b/Chrome/.gitignore index 603b5e1..dcaffc0 100644 --- a/Chrome/.gitignore +++ b/Chrome/.gitignore @@ -1,2 +1 @@ -/extension.js -/BabelExt.js +/*.js diff --git a/Chrome/manifest.json b/Chrome/manifest.json index d35165f..1c0a1a9 100644 --- a/Chrome/manifest.json +++ b/Chrome/manifest.json @@ -1,5 +1,6 @@ { "name": "BabelExt Extension", + "author": "honestbleeps", "version": "0.95", "manifest_version": 2, "description": "An extension created with BabelExt - www.babelext.com", @@ -9,20 +10,16 @@ }, "content_scripts": [ { - "matches": [ - "http://babelext.com/*" - ], - "js": ["BabelExt.js", "extension.js"] // add others here if you like, i.e. jquery... + "matches": ["http://babelext.com/*"], + "js": ["BabelExt.js", "extension.js"], // add others here if you like, i.e. jquery... + "run_at": "document_end" } ], - "icons": { - // "48": "icon48.png", - // "128": "icon128.png" - }, + "icons": {}, "permissions": [ "contextMenus", "tabs", "history", "notifications" ] -} \ No newline at end of file +} diff --git a/Firefox/data/.gitignore b/Firefox/data/.gitignore index 603b5e1..dcaffc0 100644 --- a/Firefox/data/.gitignore +++ b/Firefox/data/.gitignore @@ -1,2 +1 @@ -/extension.js -/BabelExt.js +/*.js diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4da72c4 --- /dev/null +++ b/Makefile @@ -0,0 +1,223 @@ +.PHONY: default build config clean test + +# Change this to the name of your Chrome executable: +CHROME=google-chrome + +# +# BROWSER-NEUTRAL CONFIG VALUES +# +CONFIG_FILE=lib/config.txt +ID = $(shell sed -n -e 's/\//\\\//g' -e 's/^id *//p' ${CONFIG_FILE}) +NAME = $(shell sed -n -e 's/\//\\\//g' -e 's/^name *//p' ${CONFIG_FILE}) +LICENSE = $(shell sed -n -e 's/\//\\\//g' -e 's/^license *//p' ${CONFIG_FILE}) +TITLE = $(shell sed -n -e 's/\//\\\//g' -e 's/^title *//p' ${CONFIG_FILE}) +DESCRIPTION = $(shell sed -n -e 's/\//\\\//g' -e 's/^description *//p' ${CONFIG_FILE}) +WEBSITE = $(shell sed -n -e 's/\//\\\//g' -e 's/^website *//p' ${CONFIG_FILE}) +VERSION = $(shell sed -n -e 's/\//\\\//g' -e 's/^version *//p' ${CONFIG_FILE}) +AUTHOR = $(shell sed -n -e 's/\//\\\//g' -e 's/^author *//p' ${CONFIG_FILE}) +UPDATE_CHROME = $(shell sed -n -e 's/\//\\\//g' -e 's/^update_chrome *//p' ${CONFIG_FILE}) + +SCRIPT_WHEN = $(shell sed -n -e 's/\//\\\//g' -e 's/^contentScriptWhen *//p' ${CONFIG_FILE}) +SCRIPT_FILES = $(shell sed -n -e 's/\//\\\//g' -e 's/^contentScriptFile *//p' ${CONFIG_FILE}) +SCRIPT_PROTOCOL = $(shell sed -n -e 's/\//\\\//g' -e 's/^match_protocol *//p' ${CONFIG_FILE}) +SCRIPT_DOMAIN = $(shell sed -n -e 's/\//\\\//g' -e 's/^match_domain *//p' ${CONFIG_FILE}) +SCRIPT_SUBDOMAIN = $(shell sed -n -e '/^match_include_subdomains/p' ${CONFIG_FILE}) + +ICON_16 = $(shell sed -n -e 's/\//\\\//g' -e 's/^icon_16 *//p' ${CONFIG_FILE}) +ICON_32 = $(shell sed -n -e 's/\//\\\//g' -e 's/^icon_32 *//p' ${CONFIG_FILE}) +ICON_48 = $(shell sed -n -e 's/\//\\\//g' -e 's/^icon_48 *//p' ${CONFIG_FILE}) +ICON_64 = $(shell sed -n -e 's/\//\\\//g' -e 's/^icon_64 *//p' ${CONFIG_FILE}) +ICON_128 = $(shell sed -n -e 's/\//\\\//g' -e 's/^icon_128 *//p' ${CONFIG_FILE}) + +ifeq ($(SCRIPT_SUBDOMAIN),) +SCRIPT_PRETTY_DOMAIN=$(SCRIPT_DOMAIN) +SCRIPT_SUBDOMAIN=false +else +SCRIPT_PRETTY_DOMAIN="*.$(SCRIPT_DOMAIN)" +SCRIPT_SUBDOMAIN=true +endif + +ifeq ($(SCRIPT_PROTOCOL),) +DOMAIN_FF=\"http:\/\/$(SCRIPT_PRETTY_DOMAIN)\/*\",\"https:\/\/$(SCRIPT_PRETTY_DOMAIN)\/*\" +DOMAIN_CHROME=*:\/\/$(SCRIPT_PRETTY_DOMAIN)\/* +else +DOMAIN_FF=\"$(SCRIPT_PROTOCOL):\/\/$(SCRIPT_PRETTY_DOMAIN)\/*\" +DOMAIN_CHROME=$(SCRIPT_PROTOCOL):\/\/$(SCRIPT_PRETTY_DOMAIN)\/* +endif + +comma=, +SCRIPT_FILES_FF=self.data.url('BabelExt.js')$(patsubst %,$(comma) self.data.url('%'), $(SCRIPT_FILES)) +SCRIPT_FILES_CHROME=\"BabelExt.js\"$(patsubst %,$(comma) \"%\", $(SCRIPT_FILES)) + +ifeq ($(SCRIPT_WHEN),early) +SCRIPT_WHEN_SAFARI=Start +SCRIPT_WHEN_CHROME=document_start +SCRIPT_WHEN_FF=start +SCRIPT_START_SAFARI=\ $(patsubst %,%<\/string>, $(SCRIPT_FILES))\n +else ifeq ($(SCRIPT_WHEN),late) +SCRIPT_WHEN_CHROME=document_end +SCRIPT_WHEN_FF=ready +SCRIPT_END_SAFARI=\ $(patsubst %,%<\/string>, $(SCRIPT_FILES))\n +else +SCRIPT_WHEN_SAFARI=End +SCRIPT_WHEN_CHROME=document_idle +SCRIPT_WHEN_FF=end +SCRIPT_END_SAFARI=\ $(patsubst %,%<\/string>, $(SCRIPT_FILES))\n +endif + +ICON_FILES_FF= +ICON_FILES_CHROME= +ICON_FILES= +ifneq ($(ICON_16),) +ICON_FILES_CHROME+=\"16\":\"$(ICON_16)\", +ICON_FILES+=$(ICON_16) +endif +ifneq ($(ICON_32),) +ICON_FILES_CHROME+=\"32\":\"$(ICON_32)\", +ICON_FILES+=$(ICON_32) +endif +ifneq ($(ICON_48),) +ICON_FILES_CHROME+=\"48\":\"$(ICON_48)\", +ICON_FILES+=$(ICON_48) +ICON_FILES_FF+=\n \"icon\":\"$(ICON_48)\", +endif +ifneq ($(ICON_64),) +ICON_FILES_CHROME+=\"64\":\"$(ICON_64)\", +ICON_FILES+=$(ICON_64) +ICON_FILES_FF+=\n \"icon64\":\"$(ICON_64)\", +endif +ifneq ($(ICON_128),) +ICON_FILES_CHROME+=\"128\":\"$(ICON_128)\", +ICON_FILES+=$(ICON_128) +endif + +# +# ABSTRACT TARGETS: +# + +default: makelinks.bat + test -e build || mkdir build + $(MAKE) firefox-addon-sdk build + +config: Firefox Chrome Safari.safariextension + +BUILD_TARGETS=build/$(NAME).crx build/$(NAME).chrome.zip build/$(NAME).nex build/$(NAME).xpi +build: $(BUILD_TARGETS) + @echo "\033[1;32mbuilt!\033[0m" + +clean: + rm -rf $(BUILD_TARGETS) tmp/* + + +# +# CONFIGURATION TARGETS: +# + +Firefox/lib/main.js: $(CONFIG_FILE) + sed -i \ + -e "s/\(include: \[\)[^]]*/\1$(DOMAIN_FF)/" \ + -e "s/\(contentScriptWhen: '\)[^\']*/\1$(SCRIPT_WHEN_FF)/" \ + -e "s/\(contentScriptFile: \[\)[^]]*/\1$(SCRIPT_FILES_FF)/" \ + $@ + +Firefox/package.json: $(CONFIG_FILE) + sed -i \ + -e "s/\(\"id\": *\"\)[^\"]*\"/\1$(ID)\"/" \ + -e "s/\(\"name\": *\"\)[^\"]*\"/\1$(NAME)\"/" \ + -e "s/\(\"license\": *\"\)[^\"]*\"/\1$(LICENSE)\"/" \ + -e "s/\(\"title\": *\"\)[^\"]*\"/\1$(TITLE)\"/" \ + -e "s/\(\"description\": *\"\)[^\"]*\"/\1$(DESCRIPTION)\"/" \ + -e "s/\(\"version\": *\"\)[^\"]*\"/\1$(VERSION)\"/" \ + -e "s/\(\"author\": *\"\)[^\"]*\"/\1$(AUTHOR)\"/" \ + -e "/\"icon\(64\)\?\"/d" -e "s/{/{$(ICON_FILES_FF)/" \ + $@ + +$(patsubst %,Firefox/data/%,$(SCRIPT_FILES) BabelExt.js): Firefox/data/%: lib/% + -test -e $@ && rm $@ + ln -s ../../$^ $@ + +$(patsubst %,Firefox/%,$(ICON_FILES)): Firefox/%: lib/% + -test -e $@ && rm $@ + ln -s ../$^ $@ + +Firefox: Firefox/lib/main.js Firefox/package.json $(patsubst %,Firefox/data/%,$(SCRIPT_FILES) BabelExt.js) $(patsubst %,Firefox/%,$(ICON_FILES)) + + + +Chrome/manifest.json: $(CONFIG_FILE) + sed -i \ + -e "s/\(\"name\": *\"\)[^\"]*\"/\1$(TITLE)\"/" \ + -e "s/\(\"description\": *\"\)[^\"]*\"/\1$(DESCRIPTION)\"/" \ + -e "s/\(\"version\": *\"\)[^\"]*\"/\1$(VERSION)\"/" \ + -e "s/\(\"author\": *\"\)[^\"]*\"/\1$(AUTHOR)\"/" \ + -e "s/\(\"update_url\": *\"\)[^\"]*\"/\1$(UPDATE_CHROME)\"/" \ + -e "s/\(\"matches\": *\[\"\)[^\"]*/\1$(DOMAIN_CHROME)/" \ + -e "s/\(\"icons\": *{\)[^\}]*/\1$(ICON_FILES_CHROME)/" -e 's/,}/ }/' \ + -e "s/\(\"js\": *\[\)[^]]*/\1$(SCRIPT_FILES_CHROME)/" \ + -e "s/\(\"run_at\": *\"\)[^\"]*\"/\1$(SCRIPT_WHEN_CHROME)\"/" \ + $@ + +Chrome.pem: + $(CHROME) --pack-extension=Chrome > /dev/null + rm Chrome.crx + +$(patsubst %,Chrome/%,$(SCRIPT_FILES) $(ICON_FILES) BabelExt.js): Chrome/%: lib/% + -test -e $@ && rm $@ + ln $^ $@ + +Chrome: Chrome/manifest.json Chrome.pem $(patsubst %,Chrome/%,$(SCRIPT_FILES) $(ICON_FILES) BabelExt.js) + + +Safari.safariextension/Info.plist: $(CONFIG_FILE) + sed -i \ + -e '/Author<\/key>/,// s/[^<]*<\/string>/$(AUTHOR)<\/string>/' \ + -e '/CFBundleDisplayName<\/key>/,// s/[^<]*<\/string>/$(TITLE)<\/string>/' \ + -e '/CFBundleIdentifier<\/key>/,// s/[^<]*<\/string>/com.honestbleeps.$(ID)<\/string>/' \ + -e '/CFBundleShortVersionString<\/key>/,// s/[^<]*<\/string>/$(VERSION)<\/string>/' \ + -e '/CFBundleVersion<\/key>/,// s/[^<]*<\/string>/$(VERSION)<\/string>/' \ + -e '/Description<\/key>/,// s/[^<]*<\/string>/$(DESCRIPTION)<\/string>/' \ + -e '/Website<\/key>/,// s/[^<]*<\/string>/$(WEBSITE)<\/string>/' \ + -e "s/\(Start|End\)<\/key>/$SCRIPT_WHEN_SAFARI<\/key>/" \ + -e '/Scripts<\/key>/,/Description<\/key>/ { //,/<\/dict>/ d ; s/<\/dict>/ \n End<\/key>\n \n$(SCRIPT_END_SAFARI) <\/array>\n Start<\/key>\n \n BabelExt.js<\/string>\n$(SCRIPT_START_SAFARI) <\/array>\n <\/dict>\n <\/dict>/ }' \ + $@ + +$(patsubst %,Safari.safariextension/%,$(SCRIPT_FILES) BabelExt.js): Safari.safariextension/%: lib/% + -test -L $@ && rm $@ + ln /$^ $@ + +Safari.safariextension: Safari.safariextension/config.xml $(patsubst %,Safari.safariextension/%,$(SCRIPT_FILES) BabelExt.js) + +# +# BUILD TARGETS: +# + +firefox-addon-sdk: + git clone https://github.com/mozilla/addon-sdk.git $@ + cd $@ && git checkout 1.16 + cd $@ && git archive 1.16 python-lib/cuddlefish/_version.py | tar -xvf - + +build/$(NAME).crx: Chrome lib/* Chrome.pem + $(CHROME) --pack-extension=Chrome --pack-extension-key=Chrome.pem > /dev/null + mv Chrome.crx $@ + +build/$(NAME).chrome.zip: build/$(NAME).crx + -rm -rf "tmp/$(NAME)" + mkdir -p "tmp/$(NAME)" + -cd "tmp/$(NAME)" && unzip -q ../../$^ + cd tmp && zip -rq ../$@ "$(NAME)" + rm -rf tmp + +build/$(NAME).nex: build/$(NAME).crx + cp $^ $@ + +build/$(NAME).xpi: Firefox Firefox/package.json lib/* + bash -c 'cd firefox-addon-sdk && source bin/activate && cd ../Firefox && cfx xpi' + mv Firefox/*.xpi $@ + +firefox-unpacked: build/$(NAME).xpi + test -d $@ || mkdir $@ + rm -rf $@/* + cd $@ && unzip ../$^ + rm -f $@/resources/$(NAME)/data/* + $(foreach FILE,BabelExt.js $(SCRIPT_FILES),ln -s ../../../../lib/$(FILE) $@/resources/$(NAME)/data/$(FILE) ; ) + @echo "\033[1mRemember to restart Firefox if you added/removed any files!\033[0m" diff --git a/Opera/includes/.gitignore b/Opera/includes/.gitignore index 8e35b06..dcaffc0 100644 --- a/Opera/includes/.gitignore +++ b/Opera/includes/.gitignore @@ -1,2 +1 @@ -/extension.user.js -/BabelExt.js \ No newline at end of file +/*.js diff --git a/Safari.safariextension/.gitignore b/Safari.safariextension/.gitignore index ab2f236..dcaffc0 100644 --- a/Safari.safariextension/.gitignore +++ b/Safari.safariextension/.gitignore @@ -1,2 +1 @@ -/extension.js -/BabelExt.js \ No newline at end of file +/*.js diff --git a/lib/config.txt b/lib/config.txt new file mode 100644 index 0000000..777c56d --- /dev/null +++ b/lib/config.txt @@ -0,0 +1,35 @@ +# +# BROWSER-NEUTRAL CONFIGURATION FILE +# (copied to browser-specific files during build) +# + +# In Linux, you can make a GUID by running `uuidgen` on the command line: +id abcdef01-2345-6789-9876-543210fedcba + +# General config parameters +name babelext_your_name_here +title BabelExt +description An extension created with BabelExt - www.babelext.com +license GPL +author honestbleeps +version 0.95 +update_chrome http://babelext.com/update-chrome.php +website http://babelext.com +#icon_16 icon-16.png +#icon_32 icon-32.png +#icon_48 icon-48.png +#icon_64 icon-64.png +#icon_128 icon-128.png + +# userscript config parameters: + +# contentScriptWhen can be 'early', 'middle' or 'late'. +# different browsers interpret this in different ways, but in general: +# * 'early' runs at the earliest point supported by the browser (possibly before the DOM exists) +# * 'middle' guarantees the DOM exists, but might run while the page is still loading +# * 'late' guarantees the scripts are run aft the page finishes loading +contentScriptWhen early +contentScriptFile extension.js +match_domain babelext.com +# uncomment the next line to match *.: +# match_include_subdomains diff --git a/makelinks.sh b/makelinks.sh deleted file mode 100755 index 7858450..0000000 --- a/makelinks.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -while read -r MKLINK H TO FROM -do - if [ "$MKLINK" == "mklink" ] - then - SYMLINK="" - if [ "$H" != "/H" ] - then - SYMLINK="-s" - FROM="$TO" - TO="$H" - fi - rm -f "${TO//\\//}" - ln $SYMLINK "${FROM//\\//}" "${TO//\\//}" - fi -done < makelinks.bat From 99efbf75db5da54e2aa00124394a6c3ce1789879 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 1 Sep 2014 02:14:34 +0100 Subject: [PATCH 005/105] Add example test architecture --- lib/config.txt | 6 ++++++ lib/test-helper.js | 31 +++++++++++++++++++++++++++++++ test/index.html | 17 +++++++++++++++++ test/tests.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 lib/test-helper.js create mode 100644 test/index.html create mode 100644 test/tests.js diff --git a/lib/config.txt b/lib/config.txt index 777c56d..331680d 100644 --- a/lib/config.txt +++ b/lib/config.txt @@ -29,7 +29,13 @@ website http://babelext.com # * 'middle' guarantees the DOM exists, but might run while the page is still loading # * 'late' guarantees the scripts are run aft the page finishes loading contentScriptWhen early + +# normal settings: contentScriptFile extension.js match_domain babelext.com # uncomment the next line to match *.: # match_include_subdomains + +# test settings: +#contentScriptFile test-helper.js +#match_domain localhost diff --git a/lib/test-helper.js b/lib/test-helper.js new file mode 100644 index 0000000..96cbcc3 --- /dev/null +++ b/lib/test-helper.js @@ -0,0 +1,31 @@ +/* + * Example test helper - you will probably need to edit this for yor own script + */ +(function() { + + var stored_values; + + // Run any JS specified by the test runner + $(document).on( 'click', '#rendezvous', function( event ) { + eval(this.getAttribute('data-args')); + }); + + // Mock whatever BabelExt functions you need: + BabelExt.storage = { + set: function( key, value, callback ) { + if ( callback ) { + stored_values[key] = value; + document + .getElementById('rendezvous') + .setAttribute( 'data-stored-values', JSON.stringify(stored_values) ); + callback(); + } + }, + get: function( key, callback ) { + if ( callback ) { + callback({ value: stored_values[key] }); + } + } + }; + +})(); diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..52b4702 --- /dev/null +++ b/test/index.html @@ -0,0 +1,17 @@ + + + + + Test Page + + + + +
+
+
+ + + + + diff --git a/test/tests.js b/test/tests.js new file mode 100644 index 0000000..05add0b --- /dev/null +++ b/test/tests.js @@ -0,0 +1,30 @@ +/** + * Evaluate JavaScript as ContentScript + * @param {string} js Scripnt to eval + * + * The easiest way to manipulate your contentscript + * (and sometimes the only way that doesn't involve nasty code changes) + * is to pass some JavaScript across and eval() it on the other side. + * This is nasty and insecure, which is one reason why test JS should + * be configured out before compiling your final extension. + */ +function eval_js_as_contentscript(js) { + var dispatcher = document.getElementById('rendezvous'); + dispatcher.setAttribute( 'data-args', JSON.stringify(js) ); + dispatcher.click(); +} + +/** + * Get values from BabelExt + * @return {string} + * + * By default, BabelExt is mocked by the test helper. + * Data is stored in an attribute so we can get it back. + */ +function get_stored_values() { + return document.getElementById('rendezvous').getAttribute( 'data-stored-values' ); +} + +QUnit.test( "My test", function( assert ) { + run_js_in_contentscript_context( 'my_func()' ); +}); From 503533a9215e3c18d4e6388a8154d21226bf819e Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 11 Sep 2014 06:47:53 +0100 Subject: [PATCH 006/105] Use the Firefox SDK tarball instead of the git repo addons.mozilla.org will reject packages uploaded using the git version. --- Makefile | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 4da72c4..fd6a396 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: default build config clean test +.PHONY: default build config clean test firefox-addon-sdk-url # Change this to the name of your Chrome executable: CHROME=google-chrome @@ -97,7 +97,7 @@ endif default: makelinks.bat test -e build || mkdir build - $(MAKE) firefox-addon-sdk build + $(MAKE) build config: Firefox Chrome Safari.safariextension @@ -191,10 +191,16 @@ Safari.safariextension: Safari.safariextension/config.xml $(patsubst %,Safari.sa # BUILD TARGETS: # -firefox-addon-sdk: - git clone https://github.com/mozilla/addon-sdk.git $@ - cd $@ && git checkout 1.16 - cd $@ && git archive 1.16 python-lib/cuddlefish/_version.py | tar -xvf - +firefox-addon-sdk-url: # check for new versions of the SDK + $(eval URL:=$(shell curl --silent -I https://ftp.mozilla.org/pub/mozilla.org/labs/jetpack/jetpack-sdk-latest.tar.gz | sed -ne 's/^Location: //p')) + $(eval OLD_URL:=$(shell cat $@.txt || echo)) + if [ "$(URL)" != "$(OLD_URL)" ] ; then echo "$(URL)" > $@.txt ; fi + +firefox-addon-sdk: firefox-addon-sdk-url.txt + rm -rf $@ + curl $(shell cat $^) | tar zx + mv addon-sdk-*/ firefox-addon-sdk + touch $@ build/$(NAME).crx: Chrome lib/* Chrome.pem $(CHROME) --pack-extension=Chrome --pack-extension-key=Chrome.pem > /dev/null @@ -210,7 +216,7 @@ build/$(NAME).chrome.zip: build/$(NAME).crx build/$(NAME).nex: build/$(NAME).crx cp $^ $@ -build/$(NAME).xpi: Firefox Firefox/package.json lib/* +build/$(NAME).xpi: firefox-addon-sdk-url firefox-addon-sdk Firefox Firefox/package.json lib/* bash -c 'cd firefox-addon-sdk && source bin/activate && cd ../Firefox && cfx xpi' mv Firefox/*.xpi $@ From 24a41816aad77d541f1583de607a5bbc0a633d9d Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 11 Sep 2014 07:47:05 +0100 Subject: [PATCH 007/105] Remove comment in Chrome/manifest.json Confuses the Chrome store --- Chrome/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Chrome/manifest.json b/Chrome/manifest.json index 1c0a1a9..a46c5df 100644 --- a/Chrome/manifest.json +++ b/Chrome/manifest.json @@ -11,7 +11,7 @@ "content_scripts": [ { "matches": ["http://babelext.com/*"], - "js": ["BabelExt.js", "extension.js"], // add others here if you like, i.e. jquery... + "js": ["BabelExt.js", "extension.js"], "run_at": "document_end" } ], From 927f8613a5c6e809f2f1a16669748dca59d3b344 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 11 Sep 2014 07:50:45 +0100 Subject: [PATCH 008/105] Remove Chrome update URL Disallowed by the Chrome store --- Chrome/manifest.json | 1 - Makefile | 18 ++++++++---------- lib/config.txt | 25 ++++++++++++------------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/Chrome/manifest.json b/Chrome/manifest.json index a46c5df..2332393 100644 --- a/Chrome/manifest.json +++ b/Chrome/manifest.json @@ -4,7 +4,6 @@ "version": "0.95", "manifest_version": 2, "description": "An extension created with BabelExt - www.babelext.com", - "update_url": "http://babelext.com/update-chrome.php", "background": { "scripts": ["background.js"] }, diff --git a/Makefile b/Makefile index fd6a396..2415f1f 100644 --- a/Makefile +++ b/Makefile @@ -7,15 +7,14 @@ CHROME=google-chrome # BROWSER-NEUTRAL CONFIG VALUES # CONFIG_FILE=lib/config.txt -ID = $(shell sed -n -e 's/\//\\\//g' -e 's/^id *//p' ${CONFIG_FILE}) -NAME = $(shell sed -n -e 's/\//\\\//g' -e 's/^name *//p' ${CONFIG_FILE}) -LICENSE = $(shell sed -n -e 's/\//\\\//g' -e 's/^license *//p' ${CONFIG_FILE}) -TITLE = $(shell sed -n -e 's/\//\\\//g' -e 's/^title *//p' ${CONFIG_FILE}) -DESCRIPTION = $(shell sed -n -e 's/\//\\\//g' -e 's/^description *//p' ${CONFIG_FILE}) -WEBSITE = $(shell sed -n -e 's/\//\\\//g' -e 's/^website *//p' ${CONFIG_FILE}) -VERSION = $(shell sed -n -e 's/\//\\\//g' -e 's/^version *//p' ${CONFIG_FILE}) -AUTHOR = $(shell sed -n -e 's/\//\\\//g' -e 's/^author *//p' ${CONFIG_FILE}) -UPDATE_CHROME = $(shell sed -n -e 's/\//\\\//g' -e 's/^update_chrome *//p' ${CONFIG_FILE}) +ID = $(shell sed -n -e 's/\//\\\//g' -e 's/^id *//p' ${CONFIG_FILE}) +NAME = $(shell sed -n -e 's/\//\\\//g' -e 's/^name *//p' ${CONFIG_FILE}) +LICENSE = $(shell sed -n -e 's/\//\\\//g' -e 's/^license *//p' ${CONFIG_FILE}) +TITLE = $(shell sed -n -e 's/\//\\\//g' -e 's/^title *//p' ${CONFIG_FILE}) +DESCRIPTION = $(shell sed -n -e 's/\//\\\//g' -e 's/^description *//p' ${CONFIG_FILE}) +WEBSITE = $(shell sed -n -e 's/\//\\\//g' -e 's/^website *//p' ${CONFIG_FILE}) +VERSION = $(shell sed -n -e 's/\//\\\//g' -e 's/^version *//p' ${CONFIG_FILE}) +AUTHOR = $(shell sed -n -e 's/\//\\\//g' -e 's/^author *//p' ${CONFIG_FILE}) SCRIPT_WHEN = $(shell sed -n -e 's/\//\\\//g' -e 's/^contentScriptWhen *//p' ${CONFIG_FILE}) SCRIPT_FILES = $(shell sed -n -e 's/\//\\\//g' -e 's/^contentScriptFile *//p' ${CONFIG_FILE}) @@ -150,7 +149,6 @@ Chrome/manifest.json: $(CONFIG_FILE) -e "s/\(\"description\": *\"\)[^\"]*\"/\1$(DESCRIPTION)\"/" \ -e "s/\(\"version\": *\"\)[^\"]*\"/\1$(VERSION)\"/" \ -e "s/\(\"author\": *\"\)[^\"]*\"/\1$(AUTHOR)\"/" \ - -e "s/\(\"update_url\": *\"\)[^\"]*\"/\1$(UPDATE_CHROME)\"/" \ -e "s/\(\"matches\": *\[\"\)[^\"]*/\1$(DOMAIN_CHROME)/" \ -e "s/\(\"icons\": *{\)[^\}]*/\1$(ICON_FILES_CHROME)/" -e 's/,}/ }/' \ -e "s/\(\"js\": *\[\)[^]]*/\1$(SCRIPT_FILES_CHROME)/" \ diff --git a/lib/config.txt b/lib/config.txt index 331680d..2a12903 100644 --- a/lib/config.txt +++ b/lib/config.txt @@ -7,19 +7,18 @@ id abcdef01-2345-6789-9876-543210fedcba # General config parameters -name babelext_your_name_here -title BabelExt -description An extension created with BabelExt - www.babelext.com -license GPL -author honestbleeps -version 0.95 -update_chrome http://babelext.com/update-chrome.php -website http://babelext.com -#icon_16 icon-16.png -#icon_32 icon-32.png -#icon_48 icon-48.png -#icon_64 icon-64.png -#icon_128 icon-128.png +name babelext_your_name_here +title BabelExt +description An extension created with BabelExt - www.babelext.com +license GPL +author honestbleeps +version 0.95 +website http://babelext.com +#icon_16 icon-16.png +#icon_32 icon-32.png +#icon_48 icon-48.png +#icon_64 icon-64.png +#icon_128 icon-128.png # userscript config parameters: From ae64c793f259d9388ff312ce15fe605dde4237bf Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 11 Sep 2014 07:51:14 +0100 Subject: [PATCH 009/105] Add build/chrome-store-upload.zip to the Makefile --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2415f1f..b8ae347 100644 --- a/Makefile +++ b/Makefile @@ -100,7 +100,7 @@ default: makelinks.bat config: Firefox Chrome Safari.safariextension -BUILD_TARGETS=build/$(NAME).crx build/$(NAME).chrome.zip build/$(NAME).nex build/$(NAME).xpi +BUILD_TARGETS=build/$(NAME).crx build/$(NAME).chrome.zip build/$(NAME).nex build/$(NAME).xpi build/chrome-store-upload.zip build: $(BUILD_TARGETS) @echo "\033[1;32mbuilt!\033[0m" @@ -211,6 +211,9 @@ build/$(NAME).chrome.zip: build/$(NAME).crx cd tmp && zip -rq ../$@ "$(NAME)" rm -rf tmp +build/chrome-store-upload.zip: Chrome lib/* + rm -f $@ && zip -r $@ Chrome --exclude \*~ --exclude Chrome/.gitignore + build/$(NAME).nex: build/$(NAME).crx cp $^ $@ From 7375299dbcff5e67b5fbf80f3761a10b4c217866 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 11 Sep 2014 08:11:07 +0100 Subject: [PATCH 010/105] Add website permissions for Chrome/manifest.json in Makefile --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index b8ae347..06d695e 100644 --- a/Makefile +++ b/Makefile @@ -150,6 +150,7 @@ Chrome/manifest.json: $(CONFIG_FILE) -e "s/\(\"version\": *\"\)[^\"]*\"/\1$(VERSION)\"/" \ -e "s/\(\"author\": *\"\)[^\"]*\"/\1$(AUTHOR)\"/" \ -e "s/\(\"matches\": *\[\"\)[^\"]*/\1$(DOMAIN_CHROME)/" \ + -e "s/\(\"permissions\": \[\).*/\1 \"$(DOMAIN_CHROME)\",/" \ -e "s/\(\"icons\": *{\)[^\}]*/\1$(ICON_FILES_CHROME)/" -e 's/,}/ }/' \ -e "s/\(\"js\": *\[\)[^]]*/\1$(SCRIPT_FILES_CHROME)/" \ -e "s/\(\"run_at\": *\"\)[^\"]*\"/\1$(SCRIPT_WHEN_CHROME)\"/" \ From b00ca216c134621b0e11a938a17680172993e148 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 11 Sep 2014 08:26:16 +0100 Subject: [PATCH 011/105] Clean Chrome and Firefox directories in `make clean` --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 06d695e..edac717 100644 --- a/Makefile +++ b/Makefile @@ -105,7 +105,8 @@ build: $(BUILD_TARGETS) @echo "\033[1;32mbuilt!\033[0m" clean: - rm -rf $(BUILD_TARGETS) tmp/* + rm -rf $(BUILD_TARGETS) tmp/* Firefox/data/* + find Chrome/ -type f -not -name background.js -not -name manifest.json -not -name .gitignore -exec rm '{}' ';' # From 26d05e5087a8d23c07a4bccb4b27c007f92ed628 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 12 Sep 2014 15:42:24 +0100 Subject: [PATCH 012/105] Ignore icons in Firefox and Chrome directories --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 972eba3..ec424f0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /Chrome.pem /build *~ +/Chrome/*.png +/Firefox/*.png From 031fba7d1b769e3b31ae0322bac475d9aaff7eb9 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sat, 13 Sep 2014 12:17:10 +0100 Subject: [PATCH 013/105] Add console.js --- lib/config.txt | 1 + lib/console.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 lib/console.js diff --git a/lib/config.txt b/lib/config.txt index 2a12903..48d34b6 100644 --- a/lib/config.txt +++ b/lib/config.txt @@ -31,6 +31,7 @@ contentScriptWhen early # normal settings: contentScriptFile extension.js +#contentScriptFile console.js match_domain babelext.com # uncomment the next line to match *.: # match_include_subdomains diff --git a/lib/console.js b/lib/console.js new file mode 100644 index 0000000..bf78fe6 --- /dev/null +++ b/lib/console.js @@ -0,0 +1,28 @@ +/* + * You can't log to the normal console from within a content script. + * Console logging is extremely useful during development, + * so this script makes console logging work as expected. + * Remember to remove this script in production. + */ +window.console = (function() { + + function log_in_embedded_page(command, args) { + var script = document.createElement('script'); + script.textContent = 'console.' + command + '.apply( console, ' + JSON.stringify(Array.prototype.slice.call( args, 0 )) + ')'; + document.documentElement.appendChild(script); + } + + return { + + assert: function() { return log_in_embedded_page( 'assert', arguments ) }, + + log : function() { return log_in_embedded_page( 'log' , arguments ) }, + + trace : function() { return log_in_embedded_page( 'trace' , arguments ) }, + info : function() { return log_in_embedded_page( 'info' , arguments ) }, + warn : function() { return log_in_embedded_page( 'warn' , arguments ) }, + error : function() { return log_in_embedded_page( 'error' , arguments ) } + + }; + +})(); From 14f9b54fa3ee77192585b7f7c8da61ec61023cc0 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sat, 13 Sep 2014 12:17:54 +0100 Subject: [PATCH 014/105] Ignore firefox-addon-sdk-url.txt --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ec424f0..4ac5797 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /firefox-unpacked +/firefox-addon-sdk-url.txt /firefox-addon-sdk /Chrome.pem /build From 2e565f482b5312dfb5e23e65114c3fec5f4aecfd Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sat, 13 Sep 2014 12:37:58 +0100 Subject: [PATCH 015/105] Switched to PhantomJS build script --- .gitignore | 1 + Chrome/manifest.json | 16 +- Firefox/lib/main.js | 8 +- Firefox/lib/settings.js | 3 + Firefox/package.json | 12 +- Makefile | 232 ------- README.md | 45 +- bin/build.js | 1027 +++++++++++++++++++++++++++++++ bin/build.sh | 27 + lib/config.txt | 41 -- lib/local_settings.json.example | 51 ++ lib/settings.json | 68 ++ makelinks.bat | 9 - 13 files changed, 1224 insertions(+), 316 deletions(-) create mode 100644 Firefox/lib/settings.js delete mode 100644 Makefile create mode 100755 bin/build.js create mode 100755 bin/build.sh delete mode 100644 lib/config.txt create mode 100644 lib/local_settings.json.example create mode 100644 lib/settings.json delete mode 100644 makelinks.bat diff --git a/.gitignore b/.gitignore index 4ac5797..a174dbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /firefox-unpacked /firefox-addon-sdk-url.txt /firefox-addon-sdk +/lib/local_settings.json /Chrome.pem /build *~ diff --git a/Chrome/manifest.json b/Chrome/manifest.json index 2332393..4f4b522 100644 --- a/Chrome/manifest.json +++ b/Chrome/manifest.json @@ -1,21 +1,29 @@ { - "name": "BabelExt Extension", + "name": "BabelExt", "author": "honestbleeps", "version": "0.95", "manifest_version": 2, "description": "An extension created with BabelExt - www.babelext.com", "background": { - "scripts": ["background.js"] + "scripts": [ + "background.js" + ] }, "content_scripts": [ { - "matches": ["http://babelext.com/*"], - "js": ["BabelExt.js", "extension.js"], + "matches": [ + "*://babelext.com/*" + ], + "js": [ + "BabelExt.js", + "extension.js" + ], "run_at": "document_end" } ], "icons": {}, "permissions": [ + "*://babelext.com", "contextMenus", "tabs", "history", diff --git a/Firefox/lib/main.js b/Firefox/lib/main.js index d510032..c644e0b 100644 --- a/Firefox/lib/main.js +++ b/Firefox/lib/main.js @@ -43,10 +43,12 @@ localStorage.removeItem = function(key) { delete ss.storage[key]; }; +var settings = require("./settings.js"); + pageMod.PageMod({ - include: ["*.babelext.com"], - contentScriptWhen: 'ready', - contentScriptFile: [self.data.url('BabelExt.js'), self.data.url('extension.js')], + include: settings.include, + contentScriptWhen: settings.contentScriptWhen, + contentScriptFile: settings.contentScriptFile, onAttach: function(worker) { tabs.on('activate', function(tab) { // run some code when a tab is activated... diff --git a/Firefox/lib/settings.js b/Firefox/lib/settings.js new file mode 100644 index 0000000..5d96ee2 --- /dev/null +++ b/Firefox/lib/settings.js @@ -0,0 +1,3 @@ +exports.include = ["http://babelext.com/*","https://babelext.com/*"]; +exports.contentScriptWhen = "ready"; +exports.contentScriptFile = ["self.data.url('BabelExt.js')","self.data.url('extension.js')"]; diff --git a/Firefox/package.json b/Firefox/package.json index d03a861..7ada1fd 100644 --- a/Firefox/package.json +++ b/Firefox/package.json @@ -1,9 +1,9 @@ { - "description": "An extension created with BabelExt - www.babelext.com", - "license": "GPL", - "author": "honestbleeps", - "version": "0.95", - "title": "BabelExt", - "id": "jid1-h7LuK9FSeAYouw", + "description": "An extension created with BabelExt - www.babelext.com", + "license": "GPL", + "author": "honestbleeps", + "version": "0.95", + "title": "BabelExt", + "id": "abcdef01-2345-6789-9876-543210fedcba", "name": "babelext_your_name_here" } diff --git a/Makefile b/Makefile deleted file mode 100644 index edac717..0000000 --- a/Makefile +++ /dev/null @@ -1,232 +0,0 @@ -.PHONY: default build config clean test firefox-addon-sdk-url - -# Change this to the name of your Chrome executable: -CHROME=google-chrome - -# -# BROWSER-NEUTRAL CONFIG VALUES -# -CONFIG_FILE=lib/config.txt -ID = $(shell sed -n -e 's/\//\\\//g' -e 's/^id *//p' ${CONFIG_FILE}) -NAME = $(shell sed -n -e 's/\//\\\//g' -e 's/^name *//p' ${CONFIG_FILE}) -LICENSE = $(shell sed -n -e 's/\//\\\//g' -e 's/^license *//p' ${CONFIG_FILE}) -TITLE = $(shell sed -n -e 's/\//\\\//g' -e 's/^title *//p' ${CONFIG_FILE}) -DESCRIPTION = $(shell sed -n -e 's/\//\\\//g' -e 's/^description *//p' ${CONFIG_FILE}) -WEBSITE = $(shell sed -n -e 's/\//\\\//g' -e 's/^website *//p' ${CONFIG_FILE}) -VERSION = $(shell sed -n -e 's/\//\\\//g' -e 's/^version *//p' ${CONFIG_FILE}) -AUTHOR = $(shell sed -n -e 's/\//\\\//g' -e 's/^author *//p' ${CONFIG_FILE}) - -SCRIPT_WHEN = $(shell sed -n -e 's/\//\\\//g' -e 's/^contentScriptWhen *//p' ${CONFIG_FILE}) -SCRIPT_FILES = $(shell sed -n -e 's/\//\\\//g' -e 's/^contentScriptFile *//p' ${CONFIG_FILE}) -SCRIPT_PROTOCOL = $(shell sed -n -e 's/\//\\\//g' -e 's/^match_protocol *//p' ${CONFIG_FILE}) -SCRIPT_DOMAIN = $(shell sed -n -e 's/\//\\\//g' -e 's/^match_domain *//p' ${CONFIG_FILE}) -SCRIPT_SUBDOMAIN = $(shell sed -n -e '/^match_include_subdomains/p' ${CONFIG_FILE}) - -ICON_16 = $(shell sed -n -e 's/\//\\\//g' -e 's/^icon_16 *//p' ${CONFIG_FILE}) -ICON_32 = $(shell sed -n -e 's/\//\\\//g' -e 's/^icon_32 *//p' ${CONFIG_FILE}) -ICON_48 = $(shell sed -n -e 's/\//\\\//g' -e 's/^icon_48 *//p' ${CONFIG_FILE}) -ICON_64 = $(shell sed -n -e 's/\//\\\//g' -e 's/^icon_64 *//p' ${CONFIG_FILE}) -ICON_128 = $(shell sed -n -e 's/\//\\\//g' -e 's/^icon_128 *//p' ${CONFIG_FILE}) - -ifeq ($(SCRIPT_SUBDOMAIN),) -SCRIPT_PRETTY_DOMAIN=$(SCRIPT_DOMAIN) -SCRIPT_SUBDOMAIN=false -else -SCRIPT_PRETTY_DOMAIN="*.$(SCRIPT_DOMAIN)" -SCRIPT_SUBDOMAIN=true -endif - -ifeq ($(SCRIPT_PROTOCOL),) -DOMAIN_FF=\"http:\/\/$(SCRIPT_PRETTY_DOMAIN)\/*\",\"https:\/\/$(SCRIPT_PRETTY_DOMAIN)\/*\" -DOMAIN_CHROME=*:\/\/$(SCRIPT_PRETTY_DOMAIN)\/* -else -DOMAIN_FF=\"$(SCRIPT_PROTOCOL):\/\/$(SCRIPT_PRETTY_DOMAIN)\/*\" -DOMAIN_CHROME=$(SCRIPT_PROTOCOL):\/\/$(SCRIPT_PRETTY_DOMAIN)\/* -endif - -comma=, -SCRIPT_FILES_FF=self.data.url('BabelExt.js')$(patsubst %,$(comma) self.data.url('%'), $(SCRIPT_FILES)) -SCRIPT_FILES_CHROME=\"BabelExt.js\"$(patsubst %,$(comma) \"%\", $(SCRIPT_FILES)) - -ifeq ($(SCRIPT_WHEN),early) -SCRIPT_WHEN_SAFARI=Start -SCRIPT_WHEN_CHROME=document_start -SCRIPT_WHEN_FF=start -SCRIPT_START_SAFARI=\ $(patsubst %,%<\/string>, $(SCRIPT_FILES))\n -else ifeq ($(SCRIPT_WHEN),late) -SCRIPT_WHEN_CHROME=document_end -SCRIPT_WHEN_FF=ready -SCRIPT_END_SAFARI=\ $(patsubst %,%<\/string>, $(SCRIPT_FILES))\n -else -SCRIPT_WHEN_SAFARI=End -SCRIPT_WHEN_CHROME=document_idle -SCRIPT_WHEN_FF=end -SCRIPT_END_SAFARI=\ $(patsubst %,%<\/string>, $(SCRIPT_FILES))\n -endif - -ICON_FILES_FF= -ICON_FILES_CHROME= -ICON_FILES= -ifneq ($(ICON_16),) -ICON_FILES_CHROME+=\"16\":\"$(ICON_16)\", -ICON_FILES+=$(ICON_16) -endif -ifneq ($(ICON_32),) -ICON_FILES_CHROME+=\"32\":\"$(ICON_32)\", -ICON_FILES+=$(ICON_32) -endif -ifneq ($(ICON_48),) -ICON_FILES_CHROME+=\"48\":\"$(ICON_48)\", -ICON_FILES+=$(ICON_48) -ICON_FILES_FF+=\n \"icon\":\"$(ICON_48)\", -endif -ifneq ($(ICON_64),) -ICON_FILES_CHROME+=\"64\":\"$(ICON_64)\", -ICON_FILES+=$(ICON_64) -ICON_FILES_FF+=\n \"icon64\":\"$(ICON_64)\", -endif -ifneq ($(ICON_128),) -ICON_FILES_CHROME+=\"128\":\"$(ICON_128)\", -ICON_FILES+=$(ICON_128) -endif - -# -# ABSTRACT TARGETS: -# - -default: makelinks.bat - test -e build || mkdir build - $(MAKE) build - -config: Firefox Chrome Safari.safariextension - -BUILD_TARGETS=build/$(NAME).crx build/$(NAME).chrome.zip build/$(NAME).nex build/$(NAME).xpi build/chrome-store-upload.zip -build: $(BUILD_TARGETS) - @echo "\033[1;32mbuilt!\033[0m" - -clean: - rm -rf $(BUILD_TARGETS) tmp/* Firefox/data/* - find Chrome/ -type f -not -name background.js -not -name manifest.json -not -name .gitignore -exec rm '{}' ';' - - -# -# CONFIGURATION TARGETS: -# - -Firefox/lib/main.js: $(CONFIG_FILE) - sed -i \ - -e "s/\(include: \[\)[^]]*/\1$(DOMAIN_FF)/" \ - -e "s/\(contentScriptWhen: '\)[^\']*/\1$(SCRIPT_WHEN_FF)/" \ - -e "s/\(contentScriptFile: \[\)[^]]*/\1$(SCRIPT_FILES_FF)/" \ - $@ - -Firefox/package.json: $(CONFIG_FILE) - sed -i \ - -e "s/\(\"id\": *\"\)[^\"]*\"/\1$(ID)\"/" \ - -e "s/\(\"name\": *\"\)[^\"]*\"/\1$(NAME)\"/" \ - -e "s/\(\"license\": *\"\)[^\"]*\"/\1$(LICENSE)\"/" \ - -e "s/\(\"title\": *\"\)[^\"]*\"/\1$(TITLE)\"/" \ - -e "s/\(\"description\": *\"\)[^\"]*\"/\1$(DESCRIPTION)\"/" \ - -e "s/\(\"version\": *\"\)[^\"]*\"/\1$(VERSION)\"/" \ - -e "s/\(\"author\": *\"\)[^\"]*\"/\1$(AUTHOR)\"/" \ - -e "/\"icon\(64\)\?\"/d" -e "s/{/{$(ICON_FILES_FF)/" \ - $@ - -$(patsubst %,Firefox/data/%,$(SCRIPT_FILES) BabelExt.js): Firefox/data/%: lib/% - -test -e $@ && rm $@ - ln -s ../../$^ $@ - -$(patsubst %,Firefox/%,$(ICON_FILES)): Firefox/%: lib/% - -test -e $@ && rm $@ - ln -s ../$^ $@ - -Firefox: Firefox/lib/main.js Firefox/package.json $(patsubst %,Firefox/data/%,$(SCRIPT_FILES) BabelExt.js) $(patsubst %,Firefox/%,$(ICON_FILES)) - - - -Chrome/manifest.json: $(CONFIG_FILE) - sed -i \ - -e "s/\(\"name\": *\"\)[^\"]*\"/\1$(TITLE)\"/" \ - -e "s/\(\"description\": *\"\)[^\"]*\"/\1$(DESCRIPTION)\"/" \ - -e "s/\(\"version\": *\"\)[^\"]*\"/\1$(VERSION)\"/" \ - -e "s/\(\"author\": *\"\)[^\"]*\"/\1$(AUTHOR)\"/" \ - -e "s/\(\"matches\": *\[\"\)[^\"]*/\1$(DOMAIN_CHROME)/" \ - -e "s/\(\"permissions\": \[\).*/\1 \"$(DOMAIN_CHROME)\",/" \ - -e "s/\(\"icons\": *{\)[^\}]*/\1$(ICON_FILES_CHROME)/" -e 's/,}/ }/' \ - -e "s/\(\"js\": *\[\)[^]]*/\1$(SCRIPT_FILES_CHROME)/" \ - -e "s/\(\"run_at\": *\"\)[^\"]*\"/\1$(SCRIPT_WHEN_CHROME)\"/" \ - $@ - -Chrome.pem: - $(CHROME) --pack-extension=Chrome > /dev/null - rm Chrome.crx - -$(patsubst %,Chrome/%,$(SCRIPT_FILES) $(ICON_FILES) BabelExt.js): Chrome/%: lib/% - -test -e $@ && rm $@ - ln $^ $@ - -Chrome: Chrome/manifest.json Chrome.pem $(patsubst %,Chrome/%,$(SCRIPT_FILES) $(ICON_FILES) BabelExt.js) - - -Safari.safariextension/Info.plist: $(CONFIG_FILE) - sed -i \ - -e '/Author<\/key>/,// s/[^<]*<\/string>/$(AUTHOR)<\/string>/' \ - -e '/CFBundleDisplayName<\/key>/,// s/[^<]*<\/string>/$(TITLE)<\/string>/' \ - -e '/CFBundleIdentifier<\/key>/,// s/[^<]*<\/string>/com.honestbleeps.$(ID)<\/string>/' \ - -e '/CFBundleShortVersionString<\/key>/,// s/[^<]*<\/string>/$(VERSION)<\/string>/' \ - -e '/CFBundleVersion<\/key>/,// s/[^<]*<\/string>/$(VERSION)<\/string>/' \ - -e '/Description<\/key>/,// s/[^<]*<\/string>/$(DESCRIPTION)<\/string>/' \ - -e '/Website<\/key>/,// s/[^<]*<\/string>/$(WEBSITE)<\/string>/' \ - -e "s/\(Start|End\)<\/key>/$SCRIPT_WHEN_SAFARI<\/key>/" \ - -e '/Scripts<\/key>/,/Description<\/key>/ { //,/<\/dict>/ d ; s/<\/dict>/ \n End<\/key>\n \n$(SCRIPT_END_SAFARI) <\/array>\n Start<\/key>\n \n BabelExt.js<\/string>\n$(SCRIPT_START_SAFARI) <\/array>\n <\/dict>\n <\/dict>/ }' \ - $@ - -$(patsubst %,Safari.safariextension/%,$(SCRIPT_FILES) BabelExt.js): Safari.safariextension/%: lib/% - -test -L $@ && rm $@ - ln /$^ $@ - -Safari.safariextension: Safari.safariextension/config.xml $(patsubst %,Safari.safariextension/%,$(SCRIPT_FILES) BabelExt.js) - -# -# BUILD TARGETS: -# - -firefox-addon-sdk-url: # check for new versions of the SDK - $(eval URL:=$(shell curl --silent -I https://ftp.mozilla.org/pub/mozilla.org/labs/jetpack/jetpack-sdk-latest.tar.gz | sed -ne 's/^Location: //p')) - $(eval OLD_URL:=$(shell cat $@.txt || echo)) - if [ "$(URL)" != "$(OLD_URL)" ] ; then echo "$(URL)" > $@.txt ; fi - -firefox-addon-sdk: firefox-addon-sdk-url.txt - rm -rf $@ - curl $(shell cat $^) | tar zx - mv addon-sdk-*/ firefox-addon-sdk - touch $@ - -build/$(NAME).crx: Chrome lib/* Chrome.pem - $(CHROME) --pack-extension=Chrome --pack-extension-key=Chrome.pem > /dev/null - mv Chrome.crx $@ - -build/$(NAME).chrome.zip: build/$(NAME).crx - -rm -rf "tmp/$(NAME)" - mkdir -p "tmp/$(NAME)" - -cd "tmp/$(NAME)" && unzip -q ../../$^ - cd tmp && zip -rq ../$@ "$(NAME)" - rm -rf tmp - -build/chrome-store-upload.zip: Chrome lib/* - rm -f $@ && zip -r $@ Chrome --exclude \*~ --exclude Chrome/.gitignore - -build/$(NAME).nex: build/$(NAME).crx - cp $^ $@ - -build/$(NAME).xpi: firefox-addon-sdk-url firefox-addon-sdk Firefox Firefox/package.json lib/* - bash -c 'cd firefox-addon-sdk && source bin/activate && cd ../Firefox && cfx xpi' - mv Firefox/*.xpi $@ - -firefox-unpacked: build/$(NAME).xpi - test -d $@ || mkdir $@ - rm -rf $@/* - cd $@ && unzip ../$^ - rm -f $@/resources/$(NAME)/data/* - $(foreach FILE,BabelExt.js $(SCRIPT_FILES),ln -s ../../../../lib/$(FILE) $@/resources/$(NAME)/data/$(FILE) ; ) - @echo "\033[1mRemember to restart Firefox if you added/removed any files!\033[0m" diff --git a/README.md b/README.md index 0ae8ba7..77120ec 100644 --- a/README.md +++ b/README.md @@ -61,12 +61,13 @@ thought to how to make it more universally useful. First, download all of the source from Github and put it together within a folder. -In Windows, run `makelinks.bat` to create symlinks to extension.js - these links are not -handled by github, which is why you unfortuntately have to make them yourself. -**NOTE:** You may need to open a command prompt as Administrator for this batch file to -work. +Then, download PhantomJS (http://phantomjs.org), which is used to build and deploy extensions. -In UNIX-based OSes, you can run `makelinks.sh`. Note that this will make hardlinks. +In UNIX-based OSes, run `./bin/build.sh build` to build packages, and +`./bin/build.sh release` to release them. + +The build system hasn't been tested under Windows yet - your best bet is probably to look at +the shell script and write a Windows equivalent. If it's any good, please send in a patch! **IMPORTANT OPERA NOTE:** Note that the Opera js file has .user.js in it - that's because without this, @include and @exclude directives will be ignored and your script will run on every page on @@ -84,11 +85,14 @@ recognized by Safari. Don't remove that from the name! ## Instructions for loading/testing an extension in each browser ## -### Chrome ### +- You need to build the package before you start - the initial build + process configures some files that aren't stored in git + +### Chrome / Opera ### -- Click the wrench icon and choose Tools -> Extensions +- Go to about://extensions -- Check the "Developer Mode" checkbox +- Check "Developer Mode" - Click "load unpacked extension" and choose the Chrome directory @@ -98,26 +102,19 @@ recognized by Safari. Don't remove that from the name! ### Firefox ### -- Download the Firefox Addon SDK from [https://addons.mozilla.org/en-US/developers/builder](https://addons.mozilla.org/en-US/developers/builder) - -- Follow the installation instructions there, and run the appropriate activation script (i.e. bin\activate.bat in windows) - -- Navigate to the Firefox directory under BabelExt, and type: cfx run - -- You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/) +- Go to about:addons, click the "Tools" icon in the top-right and install the add-on from file -- Further Firefox development information can be found at [https://addons.mozilla.org/en-US/developers/docs/sdk/latest/](https://addons.mozilla.org/en-US/developers/docs/sdk/latest/) +- Go to about:support and click the "Open Directory" to go to your profile directory -### Opera ### +- Open the "extensions" subdirectory and look for a subdirectory matching the "id" in your settings.json file -- Click Tools -> Extensions -> Manage Extensions +- Delete the file and replace it with a link to your extension's "firefox-unpacked" directory -- Find the config.xml file in the Opera directory of BabelExt, and drag it to the Extensions window +- Restart Firefox - You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/) -- Further Opera development information can be found at [http://dev.opera.com/addons/extensions/](http://dev.opera.com/addons/extensions/) - +- Further Firefox development information can be found at [https://addons.mozilla.org/en-US/developers/docs/sdk/latest/](https://addons.mozilla.org/en-US/developers/docs/sdk/latest/) ### Safari ### @@ -134,3 +131,9 @@ recognized by Safari. Don't remove that from the name! - You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/) - Further Safari development information can be found at [https://developer.apple.com/library/safari/#documentation/Tools/Conceptual/SafariExtensionGuide/Introduction/Introduction.html](https://developer.apple.com/library/safari/#documentation/Tools/Conceptual/SafariExtensionGuide/Introduction/Introduction.html) + +## Releasing packages ## + +You need to release the first version of your extension by hand, because each site has slightly different requirements for their extensions. + +After the initial release, fill in local_settings.json and run `bin/build.js` with the "release" command to release and update metadata. diff --git a/bin/build.js b/bin/build.js new file mode 100755 index 0000000..947aa19 --- /dev/null +++ b/bin/build.js @@ -0,0 +1,1027 @@ +#!/usr/bin/phantomjs + +/* + * Phantom JS build script + * + * Usage: phantomjs + * + * + * PhantomJS is a headless web browser, which allows us to automate + * the release process even for sites without a release API. + * Using it as a build system is a bit clunky, but less so than + * adding a second dependency + * + */ + +// Required modules: +var childProcess = require('child_process'); +var fs = require('fs'); +var system = require('system'); +var webPage = require('webpage'); + +var chrome_command = + ( system.os.name == 'windows' ) + ? 'chrome.exe' + : 'google-chrome' +; + +// script-wide debugging: +phantom.onError = function(msg, trace) { + var msgStack = ['PHANTOM ERROR: ' + msg]; + if (trace && trace.length) { + msgStack.push('TRACE:'); + trace.forEach(function(t) { + msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function +')' : '')); + }); + } + console.error(msgStack.join('\n')); + phantom.exit(1); +}; + +/* + * Create a symbolic link from source to target + */ +function symbolicLink( source, target ) { + if ( ! fs.isLink(target) ) { + if ( system.os.name == 'windows' ) { + childProcess.execFile('mklink', [target,source], function(err, stdout, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + }); + } else { + childProcess.execFile('ln', ["-s",source,target], null, function(err, stdout, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + }); + } + } +} + +/* + * Create a hard link from source to target + */ +function hardLink( source, target ) { + if ( ! fs.exists(target) ) { + if ( system.os.name == 'windows' ) { + childProcess.execFile('mklink', ['/H',target,source], function(err, stdout, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + }); + } else { + childProcess.execFile('ln', [source,target], null, function(err, stdout, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + }); + } + } +} + +/* + * Utility functions for pages + */ + +function _waitForEvent( test, callback ) { // low-level interface - see waitFor* below + + // originally based on http://newspaint.wordpress.com/2013/04/05/waiting-for-page-to-load-in-phantomjs/ + + var timeout = 10000; + var expiry = new Date().getTime() + timeout; + + var interval = setInterval(checkEvent,100); + + var page = this; + + function checkEvent() { + var failure_reason = test(this); + if ( !failure_reason ) { + clearInterval(interval); + interval = undefined; + if ( callback ) callback('success'); + return true; + } else if ( new Date().getTime() > expiry ) { + clearInterval(interval); + //callback('fail'); + console.log('Waited for ' + timeout + 'ms, but ' + failure_reason + ' - see fail.png and fail.html'); + fs.write( 'fail.html', page.content ); + page.render('fail.png'); + return program_counter.end(); + } + return false; + }; + + return checkEvent(); + +} + +function _waitForElementsPresent( selectors, callback ) { // call callback when all selectors exist on the page + + if ( typeof(selectors) == "string" ) + selectors = [ selectors ]; + + var page = this; + + return this.waitForEvent( + function() { + var missing_elements = []; + selectors.forEach( + function(selector) { + if ( ! page.evaluate(function(selector) {return document.querySelector(selector)}, selector ) ) + missing_elements.push(selector); + } + ); + if ( missing_elements.length ) + return JSON.stringify(missing_elements) + ' did not appear'; + else + return null; + }, + callback + ); + +} + +function _waitForElementsNotPresent( selectors, callback ) { // call callback when no selecters exist on the page + + if ( typeof(selectors) == "string" ) + selectors = [ selectors ]; + + var page = this; + + return this.waitForEvent( + function() { + var present_elements = []; + selectors.forEach( + function(selector) { + if ( page.evaluate(function(selector) {return document.querySelector(selector)}, selector ) ) + present_elements.push(selector); + } + ); + if ( present_elements.length ) + return JSON.stringify(present_elements) + ' remained present'; + else + return null; + }, + callback + ); + +} + +function _click(selector) { // click an HTML element + var page = this; + return this.waitForElementsPresent( + [ selector ], + function () { + page.evaluate(function(selector) { + var element = document.querySelector(selector); + var event = document.createEvent("MouseEvent"); + event.initMouseEvent( "click", true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null ); + element.dispatchEvent(event); + }, selector); + } + ); +} + +function _submit_form(submit_selector, fields, callback) { // fill in the relevant fields, then click the submit button + var page = this; + return this.waitForElementsPresent( + Object.keys(fields).concat([submit_selector]), + function() { + var file_inputs = + page.evaluate(function(fields) { + return Object.keys(fields).filter(function(key) { + var element = document.querySelector(key); + if ( element.type == 'file' ) { + return true; + } else { + element.value = fields[key]; + return false; + } + }); + }, fields); + file_inputs.forEach(function(field) { + var filename = fields[field]; + if ( filename ) { + if ( fs.exists(filename) ) { + page.uploadFile(field, filename); + } else { + console.log( "Tried to upload non-existent file: " + filename ); + phantom.exit(1); + } + } + }); + page.click(submit_selector); + if ( callback ) callback(); + } + ); +} + +function _showConsoleMessage() { // show console.log() commands from page context (enabled by default) + this.onConsoleMessage = function(message) { + if ( message.search("\n") != -1 ) + message = ( "\n" + message ).replace( /\n(.)/g, "\n\t$1" ); + system.stderr.writeLine('console: ' + message); + }; +} + +function _showResourceError() { // show errors loading resources + console.log('Started logging resorce error'); + this.onResourceError = function(resourceError) { + console.log('Unable to load resource (' + resourceError.url + ')'); + console.log('Error code: ' + resourceError.errorCode + '. Description: ' + resourceError.errorString); + }; +} +function _showResourceReceived() { // show information when resources are received from the web + console.log('Started logging resorce received'); + this.onResourceReceived = function(response) { + if ( response.stage == 'start' ) + console.log('Received ' + response.url + ': bodySize=' + response.bodySize) + }; +} + +function _hideResourceError () { console.log('Stopped logging resorce error' ); this.onResourceError = function() {} } +function _hideResourceReceived() { console.log('Stopped logging resorce received'); this.onResourceReceived = function() {} } +function _hideConsoleMessage () { this.onConsoleMessage = function() {} } + +// Initialise a page object: +function page( url, callback ) { + var page = require('webpage').create(); + page.onError = function(msg, trace) { + var msgStack = ['PAGE ERROR: ' + msg]; + if (trace && trace.length) { + msgStack.push('TRACE:'); + trace.forEach(function(t) { + msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function +')' : '')); + }); + } + console.error(msgStack.join('\n')); + phantom.exit(1); + }; + page.waitForEvent = _waitForEvent; + page.waitForElementsPresent = _waitForElementsPresent; + page.waitForElementsNotPresent = _waitForElementsNotPresent; + page.click = _click; + page.submit_form = _submit_form; + page.showResourceError = _showResourceError; + page.showResourceReceived = _showResourceReceived; + page.hideResourceError = _hideResourceError; + page.hideResourceReceived = _hideResourceReceived; + page.showConsoleMessage = _showConsoleMessage; + page.hideConsoleMessage = _hideConsoleMessage; + + page.showConsoleMessage(); + + return page.open( url, function(status) { + if (status == 'success') { + callback(page); + } else { + console.log( "Couln't connect to " + url ); + return program_counter.end(); + } + }); +} + +/* + * Keep track of asynchronous jobs, and exit when the last one finishes: + */ + +function AsyncCounter(zero_callback) { + this.count = 0; + this.zero_callback = zero_callback +} +AsyncCounter.prototype.begin = function() { ++this.count }; +AsyncCounter.prototype.end = function() { if ( !--this.count ) this.zero_callback() }; + +var program_counter = new AsyncCounter(function() { phantom.exit(0) }); + +/* + * Load settings from lib/settings.json + */ +var settings; +try { + settings = eval('('+fs.read('lib/settings.json')+')'); +} catch (e) { + console.error( + "Error in lib/settings.json: " + e + "\n" + + "Please make sure the file is formatted correctly and try again." + ); + phantom.exit(1); +} +if ( system.env.hasOwnProperty('ENVIRONMENT') ) { + var environment_specific = settings.environment_specific[ system.env.ENVIRONMENT ] || []; + Object.keys(environment_specific) + .forEach(function(property, n, properties) { + settings[ property ] = + ( Object.prototype.toString.call( settings[ property ] ) === '[object Array]' ) + ? settings[ property ].concat( environment_specific[property] ) + : environment_specific[property] + ; + }); +} else if ( settings.environment_specific ) { + console.log( + 'Please specify build environment using the ENVIRONMENT environment variable,\n' + + 'or comment out the "environment_specific" section in settings.json' + ); + phantom.exit(1); +}; +settings.contentScriptFiles.unshift('BabelExt.js'); +settings.pretty_domain = ( settings.mach_include_subdomain ? '*.' : '' ) + settings.match_domain; +delete settings.environment_specific; + +/* + * Load settings from lib/local_settings.json + */ +var local_settings; +try { + local_settings = eval('('+fs.read('lib/local_settings.json')+')'); +} catch (e) { + console.error( + "Error in lib/local_settings.json: " + e + "\n" + + "Please make sure the file is formatted correctly and try again." + ); + phantom.exit(1); +} + +function get_changelog(callback) { // call the callback with the changelog text as its only argument + + if ( !local_settings.changelog_command ) + return console.log("Please specify the changelog command"); + if ( local_settings.changelog ) + return callback(local_settings.changelog); + + childProcess.execFile( + local_settings.changelog_command[0], + local_settings.changelog_command.splice(1), + null, + function(err,changelog,stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if (err) throw err; + if ( changelog == '' ) { + console.log( "Error: empty changelog" ); + return program_counter.end(); + } else { + callback( local_settings.changelog = changelog ); + } + } + ); +} + +/* + * BUILD COMMANDS + */ + +function build_safari() { + + var when_string = { + 'early' : 'Start', + 'middle': 'End', + 'late' : 'End' + }; + + var document = new DOMParser().parseFromString(fs.read('Safari.safariextension/Info.plist'),"text/xml"); + + function get_node( key ) { + return document + .evaluate( '//dict/key[.="' + key + '"]/following-sibling::*[1]', document, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null ) + .singleNodeValue + ; + } + + function set_key( key, value ) { + get_node(key).textContent = value; + } + + get_node('Author').textContent = settings.author; + + get_node('CFBundleDisplayName' ).textContent = settings.title; + get_node('CFBundleIdentifier' ).textContent = 'com.honestbleeps.' + settings.id; + get_node('CFBundleShortVersionString').textContent = settings.version; + get_node('CFBundleVersion' ).textContent = settings.version; + get_node('Description' ).textContent = settings.description; + get_node('Website' ).textContent = settings.website; + + var start_scripts = get_node('Start'); + var end_scripts = get_node('End'); + + while (start_scripts.firstChild) start_scripts.removeChild(start_scripts.firstChild); + while ( end_scripts.firstChild) end_scripts.removeChild( end_scripts.firstChild); + + settings.contentScriptFiles.forEach(function(file) { + hardLink( 'lib/'+file, 'Safari.safariextension/' + file ) + + var script = document.createElement("string"); + script.textContent = file; + + + if ( file == 'BabelExt.js' || when_string[ settings.contentScriptWhen ] == 'Start' ) { + start_scripts.appendChild( document.createTextNode('\n\t\t\t\t') ); + start_scripts.appendChild(script); + } else { + end_scripts.appendChild( document.createTextNode('\n\t\t\t\t') ); + end_scripts.appendChild(script); + } + }); + + start_scripts.appendChild( document.createTextNode('\n\t\t\t') ); + end_scripts.appendChild( document.createTextNode('\n\t\t\t') ); + + var xml_txt = '\n' + new XMLSerializer().serializeToString(document).replace(">",">\n") + "\n"; + fs.write( 'Safari.safariextension/Info.plist', xml_txt ); + +} + +function build_firefox() { + + var when_string = { + 'early' : 'start', + 'middle': 'ready', + 'late' : 'end' + }; + + // Create settings.js: + fs.write( + 'Firefox/lib/settings.js', + 'exports.include = ["http://' + settings.pretty_domain + '/*","https://' + settings.pretty_domain + '/*"];\n' + + 'exports.contentScriptWhen = "' + when_string[settings.contentScriptWhen] + '";\n' + + 'exports.contentScriptFile = ' + JSON.stringify( settings.contentScriptFiles.map(function(file) { return "self.data.url('"+file+"')" }) ) + ";\n" + , + 'w' + ); + + // Create package.json and copy icons into place: + var pkg = { + "description": settings.description, + "license": settings.license, + "author": settings.author, + "version": settings.version, + "title": settings.title, + "id": settings.id, + "name": settings.name + }; + if (settings.icons[48]) { pkg.icon = settings.icons[48]; symbolicLink( '../lib/'+pkg.icon , 'Firefox/'+pkg.icon ); } + if (settings.icons[64]) { pkg.icon_64 = settings.icons[64]; symbolicLink( '../lib/'+pkg.icon_64, 'Firefox/'+pkg.icon_64 ); } + fs.write( 'Firefox/package.json', JSON.stringify(pkg, null, ' ' ) + "\n", 'w' ); + + // Copy scripts into place: + settings.contentScriptFiles.forEach(function(file) { symbolicLink( '../../lib/'+file, 'Firefox/data/' + file ) }); + + program_counter.begin(); + + // Check whether the Addon SDK is up-to-date: + var page = webPage.create(); + page.onResourceReceived = function(response) { + if ( fs.exists('firefox-addon-sdk-url.txt') && fs.read('firefox-addon-sdk-url.txt') == response.redirectURL ) { + console.log( 'Firefox Addon SDK is up-to-date.' ); + build_xpi(); + } else { + console.log( 'Downloading Firefox Addon SDK...' ); + // PhantomJS refuses to download any file as large as the SDK (I think it's either about the encoding or the file size) + // do it with `curl` instead: + console.log( 'Unpacking Firefox Addon SDK...', status ); + childProcess.execFile( 'curl', ['--silent',response.redirectURL,'-o','temporary_file.tar.gz'], null, function(err, stdout, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + fs.makeDirectory('firefox-addon-sdk'); + childProcess.execFile( 'tar', ["zxf",'temporary_file.tar.gz','-C','firefox-addon-sdk','--strip-components=1'], null, function(err,stdout,stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + fs.remove('temporary_file.tar.gz'); + fs.write( 'firefox-addon-sdk-url.txt', response.redirectURL, 'w' ); + build_xpi(); + }); + }); + } + page.stop(); // TODO: check which of these two we need + page.close(); + }; + page.openUrl('https://ftp.mozilla.org/pub/mozilla.org/labs/jetpack/addon-sdk-latest.tar.gz', 'HEAD', page.settings); + + // Build the .xpi file: + function build_xpi() { + if ( system.os.name == 'windows' ) { + // TODO: fill in real Windows values here (the following line is just a guess): + childProcess.execFile( 'cmd' , [ 'cd firefox-addon-sdk ; bin\activate ; cd ../Firefox ; cfx xpi'], null, finalise_xpi ); + } else { + childProcess.execFile( 'bash', ['-c','cd firefox-addon-sdk && source bin/activate && cd ../Firefox && cfx xpi'], null, finalise_xpi ); + } + } + + // Move the .xpi into place, fix its install.rdf, and update firefox-unpacked: + function finalise_xpi(err, stdout, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + fs.makeDirectory('build'); + var xpi = 'build/' + settings.name + '.xpi'; + if ( fs.exists(xpi) ) fs.remove(xpi); + fs.list('Firefox').forEach(function(file) { if ( file.search(/\.xpi$/) != -1 ) fs.move( 'Firefox/' + file, xpi ); }); + fs.removeTree('firefox-unpacked'); + fs.makeDirectory('firefox-unpacked'); + childProcess.execFile( 'unzip', ['-d','firefox-unpacked',xpi], null, function(err,stdout,stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + fs.write( + 'firefox-unpacked/install.rdf', + fs.read('firefox-unpacked/install.rdf').replace( /.*<\/em:maxVersion>/, '' + settings.firefox_max_version + '' ) + ); + settings.contentScriptFiles.forEach(function(file) { + fs.remove('firefox-unpacked/resources/'+settings.name+'/data/'+file); + symbolicLink( '../../../../lib/'+file, 'firefox-unpacked/resources/'+settings.name+'/data/'+file ) + }); + fs.changeWorkingDirectory('firefox-unpacked'); + childProcess.execFile( 'zip', ['../'+xpi,'install.rdf'], null, function(err,stdout,stderr) { + fs.changeWorkingDirectory('..'); + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + console.log('Built ' + xpi + '\n\033[1mRemember to restart Firefox if you added/removed any files!\033[0m'); + return program_counter.end(); + }); + }); + } + +} + +function build_chrome() { + + var when_string = { + 'early' : 'document_start', + 'middle': 'document_end', + 'late' : 'document_idle' + }; + + // Create manifest.json: + fs.write( + 'Chrome/manifest.json', + JSON.stringify({ + "name": settings.title, + "author": settings.author, + "version": settings.version, + "manifest_version": 2, + "description": settings.description, + "background": { + "scripts": ["background.js"] + }, + "content_scripts": [ + { + "matches": [ "*://" + settings.pretty_domain + '/*' ], + "js": settings.contentScriptFiles, + "run_at": when_string[settings.contentScriptWhen] + } + ], + "icons": settings.icons, + "permissions": [ + "*://" + settings.pretty_domain + "/*", + "contextMenus", + "tabs", + "history", + "notifications" + ] + }, null, '\t' ) + "\n", + 'w' + ); + + // Copy scripts and icons into place: + settings.contentScriptFiles.forEach(function(file) { hardLink( 'lib/'+file , 'Chrome/' + file ) }); + Object.keys(settings.icons).forEach(function(key ) { hardLink( 'lib/'+settings.icons[key], 'Chrome/' + settings.icons[key] ) }); + + program_counter.begin(); + + // Create a Chrome key: + if (fs.exists('Chrome.pem')) { + build_crx(); + } else { + childProcess.execFile(chrome_command, ["--pack-extension=Chrome"], null, function (err, stdout, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + build_crx(); + }); + }; + + // Build the .crx, move it into place, and build the upload zip file: + function build_crx() { + childProcess.execFile(chrome_command, ["--pack-extension=Chrome","--pack-extension-key=Chrome.pem"], null, function (err, stdout, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( stdout != 'Created the extension:\n\nChrome.crx\n' ) console.log(stdout.replace(/\n$/,'')); + var crx = 'build/' + settings.name + '.crx'; + if ( fs.exists(crx) ) fs.remove(crx); + fs.move( 'Chrome.crx', crx ); + console.log('Built ' + crx); + if ( fs.exists('build/chrome-store-upload.zip') ) fs.remove('build/chrome-store-upload.zip'); + childProcess.execFile( + 'zip', + ['build/chrome-store-upload.zip','Chrome/background.js','Chrome/manifest.json'] + .concat( settings.contentScriptFiles.map(function(file) { return 'Chrome/'+file }) ) + .concat( Object.keys(settings.icons).map(function(key ) { return 'Chrome/' + settings.icons[key] }) ) + , + null, + function(err,stdout,stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + console.log('Built build/chrome-store-upload.zip'); + return program_counter.end(); + } + ); + }); + }; + +} + + +/* + * RELEASE COMMANDS + */ + +function release_amo(login_info) { + + program_counter.begin(); + if ( !login_info.password ) { + if ( system.env.hasOwnProperty('AMO_PASSWORD') ) { + login_info.password = system.env.AMO_PASSWORD; + } else { + console.log("Please specify a password for addons.mozilla.org"); + return program_counter.end(); + } + } + + var name = settings.name.substr(0,30); + + page( 'https://addons.mozilla.org/en-US/developers/addon/' + name + '/edit', function(page) { get_changelog(function(changelog) { + + page.evaluate( function() { Tabzilla.disableEasterEgg() }); + + page.submit_form( + "#login-submit", + { + "#id_username": login_info.username, + "#id_password": login_info.password, + } + ); + + page.waitForElementsPresent( + [ '#edit-addon-basic a.button', '#edit-addon-media a.button' ], + function() { + setTimeout(function() { + + function submit_section(section, values) { + // Doing the AJAX request manually turns out less hassle than clicking the buttons: + page.evaluate(function(addon, section, values) { + $.ajax({ + async: false, + url: 'https://addons.mozilla.org/en-US/developers/addon/' + addon + '/edit_' + section + '/edit', + dataType: 'html', + success: function(html) { + var data = {}; + $(html).find('[name]').each(function() { + if ( $(this).filter(':radio,:checkbox').length ) { + if ( $(this).prop('checked') ) { + if ( !data.hasOwnProperty($(this).attr('name')) ) + data[ $(this).attr('name') ] = [ $(this).val() ]; + else + data[ $(this).attr('name') ].push( $(this).val() ); + } + } else { + data[ $(this).attr('name') ] = $(this).val(); + }; + }); + Object.keys(values).forEach(function(key) { + data[key] = values[key]; + }); + $.ajax({ + async: false, + type: "POST", + url: 'https://addons.mozilla.org/en-US/developers/addon/' + addon + '/edit_' + section + '/edit', + headers: { 'X-CSRFToken': $('meta[name=csrf]').attr('content') }, + data: data, + dataType: 'html', + //success: function(html) { console.log(html) }, + traditional: true + }); + } + }); + }, name, section, values); + } + + submit_section( 'basic', { + 'form-INITIAL_FORMS': 1, + 'form-MAX_NUM_FORMS': 1000, + 'form-TOTAL_FORMS' : 1, + 'name_en-us' : settings.title, + 'slug' : settings.name.substr(0,30), + 'summary_en-us' : settings.description, + }); + + submit_section( 'details', { + 'description_en-us': settings.long_description + }); + + var best_icon = settings.icons[64] || settings.icons[128] || settings.icons[32] || settings.icons[48] || settings.icons[16]; + if ( best_icon ) { + page.click('#edit-addon-media a.button'); + page.waitForElementsPresent( + [ '#id_icon_upload', '.edit-media-button.listing-footer button' ], + function() { + setTimeout(function() { + page.submit_form( + '#id_icon_upload', + { + '#id_icon_upload': 'lib/'+best_icon + }, + function() { + setTimeout(function() { + page.click('.edit-media-button.listing-footer button'); + page.waitForElementsNotPresent('#id_icon_upload', function() { + release_new_version(); + }) + }, 2000); + } + ); + }, 2000 ); + } + ) + } else { + release_new_version(); + }; + + }, 1000 ); + + } + ); + + function release_new_version() { + page.open( 'https://addons.mozilla.org/en-US/developers/addon/' + name + '/versions#version-upload', function() { + page.submit_form( + '#upload-addon', + { + '#upload-addon': 'build/' + settings.name + '.xpi' + }, + function() { + page.waitForElementsPresent( + '#upload-status-results.status-pass', + function() { + page.click('#upload-file-finish'); + page.submit_form( + '.listing-footer button[type="submit"]', + { + '#id_releasenotes_0': changelog + }, + function() { + page.waitForElementsPresent( + '.notification-box.success', + function() { + console.log('Released to https://addons.mozilla.org/en-US/firefox/addon/' + name); + return program_counter.end(); + } + ); + } + ); + } + ); + } + ); + }); + } + })}); + +} + +function release_chrome(login_info) { + + program_counter.begin(); + if ( !login_info.password ) { + if ( system.env.hasOwnProperty('CHROME_PASSWORD') ) { + login_info.password = system.env.CHROME_PASSWORD; + } else { + console.log("Please specify a password for the Chrome store"); + return program_counter.end(); + } + } + + page( 'https://chrome.google.com/webstore/developer/edit/' + login_info.id, function(page) { + + page.hideConsoleMessage(); + + page.submit_form( + "#signIn", + { + "#Email" : login_info.username, + "#Passwd": login_info.password, + }, + function() { change_details(page) } + ); + + }); + + function change_details(page) { + + if ( settings.icons[128] ) { + page.click(".id-upload-icon-image"); + setTimeout(function() { + page.submit_form( + '.id-upload-image.cx-bold', + { + '#cx-img-uploader-input': 'lib/' + settings.icons[128] + }, + function() { + page.waitForElementsPresent( + [ 'b#cx-error-html' ], + function() { + setTimeout( publish_details, 1000 ); + } + ); + } + ); + }, 100 ); + } else { + publish_details(); + } + + function publish_details() { + page.submit_form( + '.id-publish', + { + '#cx-dev-edit-desc': settings.description + }, + function() { + setTimeout(function() { + page.click('.id-confirm-dialog-publish-ok'); + page.waitForElementsPresent( + [ '#hist_state' ], + get_access_code + ) + }, 100 ); + } + ); + } + + }; + + function get_access_code() { + + page( + 'https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id=' + login_info.client_id, + function(page) { + + page.waitForElementsPresent( + '#submit_approve_access', + function() { + page.hideConsoleMessage(); + setTimeout( + function() { + page.click('#submit_approve_access'); + page.waitForElementsPresent( + '#code', + function() { + get_auth_key( page.evaluate(function() { return document.getElementById('code').value }) ); + } + ) + }, + 3000 + ); + } + ); + + } + ); + + function get_auth_key(code) { + var page = webPage.create(); + var post_data = "grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id=" + login_info.client_id + "&client_secret=" + login_info.client_secret + "&code=" + code; + + // PhantomJS refuses to download chunked data, do it with `curl` instead: + childProcess.execFile( 'curl', ["--silent","https://accounts.google.com/o/oauth2/token",'-d',post_data], null, function(err, json, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + upload_and_publish( JSON.parse(json) ); + }); + } + + function upload_and_publish(data) { + + var page = webPage.create(); + page.customHeaders = { + "Authorization": "Bearer " + data.access_token, + "x-goog-api-version": 2, + }; + + page.open( + "https://www.googleapis.com/upload/chromewebstore/v1.1/items/" + login_info.id, + 'PUT', + fs.open('build/chrome-store-upload.zip', 'rb').read(), + function (status) { + if ( status == "success" ) { + var result = JSON.parse(page.plainText); + if ( result.error ) { + console.log( page.plainText ); + return program_counter.end(); + } + page.open( + "https://www.googleapis.com/chromewebstore/v1.1/items/" + login_info.id + "/publish", + 'POST', + '', + function (status) { + if ( result.error ) { + console.log( page.plainText ); + return program_counter.end(); + } + if ( status == "success" ) { + console.log('Released to https://chrome.google.com/webstore/detail/' + login_info.id); + return program_counter.end(); + } else { + console.log( "Couln't upload new version" ); + return program_counter.end(); + } + } + ); + } + } + ); + + } + + } + +} + +function release_opera(login_info) { + + program_counter.begin(); + if ( !login_info.password ) { + if ( system.env.hasOwnProperty('OPERA_PASSWORD') ) { + login_info.password = system.env.OPERA_PASSWORD; + } else { + console.log("Please specify a password for the Opera Developer site"); + return program_counter.end(); + } + } + + get_changelog(function(changelog) { + + page( 'https://addons.opera.com/en-gb/developer/upgrade/' + settings.name, function(page) { + + page.submit_form( + 'button[type="submit"]', + { + "#login-page-username": login_info.username, + "#login-page-password": login_info.password, + } + ); + + page.submit_form( + '.submit-button', + { + '#id_package_file': 'build/' + settings.name + '.crx' + }, + function() { + page.submit_form( + '.submit-button', + { + '#id_translations-0-short_description': settings.description, + '#id_translations-0-long_description' : settings.long_description, + '#id_translations-0-changelog' : changelog, + '#id_target_platform-comment' : login_info.tested_on, + '#id_icons-0-icon' : settings.icons[64] ? 'lib/' + settings.icons[64] : undefined, + }, + function() { + page.click('input.submit-button[type="submit"][name="approve_widget"]'); + page.waitForElementsPresent( + [ '#dev-sel-container' ], + function() { + console.log('Released to https://addons.opera.com/en-gb/extensions/details/' + settings.name); + return program_counter.end(); + } + ); + } + ); + } + ); + }); + + }); + +} + +/* + * MAIN SECTION + */ + +var args = system.args; + +function usage() { + console.log( + 'Usage: ' + args[0] + ' []\n' + + 'Commands:\n' + + ' build - builds extensions for all browsers\n' + + ' release - release extension to either "amo" (addons.mozilla.org) "chrome" (Chrome store) or "opera" (Opera site)' + ); + phantom.exit(1); +} + +program_counter.begin(); +switch ( args[1] || '' ) { + +case 'build': + if ( args.length != 2 ) usage(); + build_safari(); + build_firefox(); + build_chrome (); + break; + +case 'release': + if ( args.length != 3 ) usage(); + switch ( args[2] ) { + case 'amo' : release_amo (local_settings. amo_login_info); break; + case 'chrome': release_chrome(local_settings.chrome_login_info); break; + case 'opera' : release_opera (local_settings. opera_login_info); break; + } + break; + +default: + usage(); + +} +program_counter.end(); diff --git a/bin/build.sh b/bin/build.sh new file mode 100755 index 0000000..cf9d957 --- /dev/null +++ b/bin/build.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +if [ "$1" == "release" ] +then + if grep -q chrome_info lib/local_settings.json && [ -z "$CHROME_PASSWORD" ] + then + read -p 'Password for chrome.google.com: ' -s CHROME_PASSWORD + echo + fi + export CHROME_PASSWORD + + if grep -q amo_info lib/local_settings.json && [ -z "$AMO_PASSWORD" ] + then + read -p 'Password for addons.mozilla.org: ' -s AMO_PASSWORD + echo + fi + export AMO_PASSWORD + + if grep -q opera_info lib/local_settings.json && [ -z "$OPERA_PASSWORD" ] + then + read -p 'Password for developer.opera.com: ' -s OPERA_PASSWORD + echo + fi + export OPERA_PASSWORD +fi + +exec ./build.js "$@" \ No newline at end of file diff --git a/lib/config.txt b/lib/config.txt deleted file mode 100644 index 48d34b6..0000000 --- a/lib/config.txt +++ /dev/null @@ -1,41 +0,0 @@ -# -# BROWSER-NEUTRAL CONFIGURATION FILE -# (copied to browser-specific files during build) -# - -# In Linux, you can make a GUID by running `uuidgen` on the command line: -id abcdef01-2345-6789-9876-543210fedcba - -# General config parameters -name babelext_your_name_here -title BabelExt -description An extension created with BabelExt - www.babelext.com -license GPL -author honestbleeps -version 0.95 -website http://babelext.com -#icon_16 icon-16.png -#icon_32 icon-32.png -#icon_48 icon-48.png -#icon_64 icon-64.png -#icon_128 icon-128.png - -# userscript config parameters: - -# contentScriptWhen can be 'early', 'middle' or 'late'. -# different browsers interpret this in different ways, but in general: -# * 'early' runs at the earliest point supported by the browser (possibly before the DOM exists) -# * 'middle' guarantees the DOM exists, but might run while the page is still loading -# * 'late' guarantees the scripts are run aft the page finishes loading -contentScriptWhen early - -# normal settings: -contentScriptFile extension.js -#contentScriptFile console.js -match_domain babelext.com -# uncomment the next line to match *.: -# match_include_subdomains - -# test settings: -#contentScriptFile test-helper.js -#match_domain localhost diff --git a/lib/local_settings.json.example b/lib/local_settings.json.example new file mode 100644 index 0000000..a471a47 --- /dev/null +++ b/lib/local_settings.json.example @@ -0,0 +1,51 @@ +/* + * Local settings + * This contains settings that are specific to an installation, + * Rename this file to 'local_settings.json' before use + * 'localsettings.json' is in .gitignore because it should never go in version control + */ +{ + + // set this to release to addons.mozilla.org: + amo_login_info: { + username: 'user@example.org', + password: 'password123' // optional - defaults to the environment variable 'AMO_PASSWORD' + }, + + // set this to release to the Chrome store: + chrome_login_info: { + + username: 'user@example.org' + password: 'password123', // optional - defaults to the environment variable 'CHROME_PASSWORD' + + // "ID" string in chrome://extensions/: + id: "abcdefghijklmnopqrstuvwxyz012345", + + /* + * Keys for the Chrome WebStore API. + * Follow the instructions here: https://developer.chrome.com/webstore/using_webstore_api + * + * Make sure to get an access token (code) manually once, to make sure it works. + * If you get a 401 error when you try to retrieve the token, go to APIs & auth > Consent screen + * in the Google Developers Console and fill in your email address and product name. + * + * Also make sure to enable the Web Store API by going to APIs & auth > APIs, + * browsing for "Chrome Web Store API" and changing the status to "ON" + */ + "client_id" : 'abcdefghijkl-mnopqrstuvwxyz0123456789ABCDEFGH.apps.googleusercontent.com', + "client_secret": "abcdefghijklm-nopqrstuvw", + + }, + + // set this to release to the Opera extensions site: + opera_login_info: { + username: 'user@example.org', + password: 'password123', // optional - defaults to the environment variable 'OPERA_PASSWORD' + tested_on: 'Opera 25 Developer Linux' // List of versions you've tested on (see opera://about for your version) + } + + // Command to get the changelog for your latest version. + // At the time of writing, this was required by Opera and hadn't yet been implemented for other browsers: + changelog_command: [ 'git', 'log', '--pretty=* %s', '@{u}..HEAD', '--reverse', '--first-parent' ], + +} \ No newline at end of file diff --git a/lib/settings.json b/lib/settings.json new file mode 100644 index 0000000..39c8fc6 --- /dev/null +++ b/lib/settings.json @@ -0,0 +1,68 @@ +/* + * BROWSER-NEUTRAL CONFIGURATION FILE + * (copied to browser-specific files during build) + */ +{ + // In Linux, you can make a GUID by running `uuidgen` on the command line: + "id": "abcdef01-2345-6789-9876-543210fedcba", + + // General config parameters + "name": "babelext_your_name_here", + "title": "BabelExt", + "description": "An extension created with BabelExt - www.babelext.com", + "license": "GPL", + "author": "honestbleeps", + "version": "0.95", + "website": "http://babelext.com", + "icons": { + /* + 16: "icon-16.png", + 32: "icon-32.png", + 48: "icon-48.png", + 64: "icon-64.png", + 128: "icon-128.png" + */ + }, + + "long_description": + "This extension uses the BabelExt cross-browser development framework.\n" + + "Go to http://www.babelext.com for more information.", + + // userscript config parameters: + + /* + * contentScriptWhen can be 'early', 'middle' or 'late'. + * different browsers interpret this in different ways, but in general: + * * 'early' runs at the earliest point supported by the browser (possibly before the DOM exists) + * * 'middle' guarantees the DOM exists, but might run while the page is still loading + * * 'late' guarantees the scripts are run aft the page finishes loading + */ + "contentScriptWhen": "middle", + + "contentScriptFiles": [ "extension.js" ], + "match_domain": "babelext.com", + // whether to match *.: + "match_include_subdomains": false, + + "environment_specific": { + /* + * If you set the "ENVIRONMENT" environment variable, + * variables from the relevant block will be used: + */ + "development": { + "contentScriptFiles": [ "console.js" ], + }, + + "test": { + "contentScriptFiles": [ "test-helper.js" ], + }, + + "production": { + "match_domain": "babelext.com", + } + + }, + + "firefox_max_version": '32.*' + +} diff --git a/makelinks.bat b/makelinks.bat deleted file mode 100644 index ee4006a..0000000 --- a/makelinks.bat +++ /dev/null @@ -1,9 +0,0 @@ -mklink Chrome\extension.js ..\lib\extension.js -mklink Opera\includes\extension.user.js ..\..\lib\extension.js -mklink Firefox\data\extension.js ..\..\lib\extension.js -mklink /H Safari.safariextension\extension.js lib\extension.js - -mklink Chrome\BabelExt.js ..\lib\BabelExt.js -mklink Opera\includes\BabelExt.js ..\..\lib\BabelExt.js -mklink Firefox\data\BabelExt.js ..\..\lib\BabelExt.js -mklink /H Safari.safariextension\BabelExt.js lib\BabelExt.js \ No newline at end of file From f69b2838907326d19be023969a6ef20190450a31 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 18 Sep 2014 09:48:17 +0100 Subject: [PATCH 016/105] Fix path for build.js in build.sh --- bin/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/build.sh b/bin/build.sh index cf9d957..529bbb2 100755 --- a/bin/build.sh +++ b/bin/build.sh @@ -24,4 +24,4 @@ then export OPERA_PASSWORD fi -exec ./build.js "$@" \ No newline at end of file +exec ./bin/build.js "$@" From a3e35f5754c646818da7b4a00fbcd206fe75f8d5 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 22 Sep 2014 02:31:23 +0100 Subject: [PATCH 017/105] Die if a non-existent environment is specified --- bin/build.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bin/build.js b/bin/build.js index 947aa19..656ce80 100755 --- a/bin/build.js +++ b/bin/build.js @@ -302,7 +302,14 @@ try { phantom.exit(1); } if ( system.env.hasOwnProperty('ENVIRONMENT') ) { - var environment_specific = settings.environment_specific[ system.env.ENVIRONMENT ] || []; + var environment_specific = settings.environment_specific[ system.env.ENVIRONMENT ]; + if ( !environment_specific ) { + console.log( + 'Please specify one of the following build environments: ' + + Object.keys(settings.environment_specific).join(' ') + ); + phantom.exit(1); + } Object.keys(environment_specific) .forEach(function(property, n, properties) { settings[ property ] = From 992a11778613fb367aec308a097bd705345d11d6 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 22 Sep 2014 02:31:48 +0100 Subject: [PATCH 018/105] Fix contentScriptFile in Firefox --- Firefox/lib/main.js | 2 +- bin/build.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Firefox/lib/main.js b/Firefox/lib/main.js index c644e0b..7f8c6a7 100644 --- a/Firefox/lib/main.js +++ b/Firefox/lib/main.js @@ -48,7 +48,7 @@ var settings = require("./settings.js"); pageMod.PageMod({ include: settings.include, contentScriptWhen: settings.contentScriptWhen, - contentScriptFile: settings.contentScriptFile, + contentScriptFile: settings.contentScriptFile.map(function(file) { return self.data.url(file) }), onAttach: function(worker) { tabs.on('activate', function(tab) { // run some code when a tab is activated... diff --git a/bin/build.js b/bin/build.js index 656ce80..5be3c5b 100755 --- a/bin/build.js +++ b/bin/build.js @@ -444,7 +444,7 @@ function build_firefox() { 'Firefox/lib/settings.js', 'exports.include = ["http://' + settings.pretty_domain + '/*","https://' + settings.pretty_domain + '/*"];\n' + 'exports.contentScriptWhen = "' + when_string[settings.contentScriptWhen] + '";\n' + - 'exports.contentScriptFile = ' + JSON.stringify( settings.contentScriptFiles.map(function(file) { return "self.data.url('"+file+"')" }) ) + ";\n" + 'exports.contentScriptFile = ' + JSON.stringify(settings.contentScriptFiles) + ";\n" , 'w' ); From 2eeaa09bb769710edf250dcccbc88cae022929e4 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 25 Sep 2014 17:37:34 +0100 Subject: [PATCH 019/105] Remove support for pre-Blink Opera Modern versions of Opera use Blink, so are similar enouh to Chrome not to need special treatment. Older versions of Opera had very primitive extension support. Removing support for older versions of Opera allows us to support more functionality with less work. --- Opera/config.xml | 9 --- Opera/includes/.gitignore | 1 - Opera/index.html | 133 -------------------------------------- Opera/notification.html | 68 ------------------- Opera/notification.png | Bin 414 -> 0 bytes README.md | 4 -- lib/BabelExt.js | 64 +----------------- lib/extension.js | 14 ---- sink.html | 3 +- 9 files changed, 4 insertions(+), 292 deletions(-) delete mode 100644 Opera/config.xml delete mode 100644 Opera/includes/.gitignore delete mode 100644 Opera/index.html delete mode 100644 Opera/notification.html delete mode 100644 Opera/notification.png diff --git a/Opera/config.xml b/Opera/config.xml deleted file mode 100644 index 90a9838..0000000 --- a/Opera/config.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - BabelExt Extension - An extension created with BabelExt - www.babelext.com - - Steve Sobel - - - diff --git a/Opera/includes/.gitignore b/Opera/includes/.gitignore deleted file mode 100644 index dcaffc0..0000000 --- a/Opera/includes/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/*.js diff --git a/Opera/index.html b/Opera/index.html deleted file mode 100644 index 6d22bc6..0000000 --- a/Opera/index.html +++ /dev/null @@ -1,133 +0,0 @@ - - \ No newline at end of file diff --git a/Opera/notification.html b/Opera/notification.html deleted file mode 100644 index 2379514..0000000 --- a/Opera/notification.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - -
    -
- - \ No newline at end of file diff --git a/Opera/notification.png b/Opera/notification.png deleted file mode 100644 index 4abc65bcd042a0fae0fef36e2547a468eab4539b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 414 zcmV;P0b%}$P)nhF)pxFXbZOIL!798 ziw8wwZ4e`9$hE*MYB1!CUE$K{pCgO)-#XB>77JL$jq}0^s=X-3aHl#siy~Onb<&$$ zrG&ceUdOr-MbM1_q?IhU?Q4#m#G5lduDqn};i$5m$x#!Qk4Eq1n96$>{c-I%p}kp> z%JGe79i2thW;KHkd@9*i-kY|-oT_cD$RBYB(?O}f^N#=n06Bt0J*b#!rvLx|07*qo IM6N<$f+D=W(f|Me diff --git a/README.md b/README.md index 77120ec..91e0862 100644 --- a/README.md +++ b/README.md @@ -69,10 +69,6 @@ In UNIX-based OSes, run `./bin/build.sh build` to build packages, and The build system hasn't been tested under Windows yet - your best bet is probably to look at the shell script and write a Windows equivalent. If it's any good, please send in a patch! -**IMPORTANT OPERA NOTE:** Note that the Opera js file has .user.js in it - that's because without this, -@include and @exclude directives will be ignored and your script will run on every page on -the internet! - **IMPORTANT SAFARI NOTE:** Safari has a "security feature" that is not documented, gives no user feedback at all, and can be a HUGE time sink if you don't know about it! If you have any files in your extension folder that are symlinks, Safari will **silently** ignore them. diff --git a/lib/BabelExt.js b/lib/BabelExt.js index bce9116..a08b163 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -16,7 +16,6 @@ var BabelExt = (function(Global, unsafeGlobal) { var console = Global.console ? Global.console:{log:function(){}}; var document = Global.document; var chrome = Global.chrome; - var opera = Global.opera; var safari = Global.safari; if (typeof(self.on) !== 'undefined') { @@ -66,43 +65,6 @@ var BabelExt = (function(Global, unsafeGlobal) { } } ); - } else if (typeof(opera) !== 'undefined') { - // opera - instance.detectedBrowser = 'Opera'; - /* - * Opera compatibility hacks - * - * Note: it's not recommended you use the localStorage function since that can easily be deleted - * when the user clears history, etc. localStorage on its own is tied to the domain name - * of the site and is more likely to be cleared. You should instead use the BabelExt.storage API. - * - */ - var localStorage = Global.localStorage; - var location = Global.location; - var XMLHttpRequest = Global.XMLHttpRequest; - - // console.log = opera.postError; - // listen for messages from opera's background page... - // This is the message handler for Opera - the background page calls this function with return data... - instance.operaMessageHandler = function(msgEvent) { - var eventData = msgEvent.data; - switch (eventData.msgType) { - case 'xmlhttpRequest': - // Fire the appropriate onload function for this xmlhttprequest. - instance.callbackQueue.callbacks[eventData.callbackID](eventData.data); - break; - case 'localStorage': - instance.callbackQueue.callbacks[eventData.callbackID](eventData); - break; - case 'createTab': - // we don't need to do anything here, but we could if we wanted to add something... - break; - default: - console.log('unknown event type in operaMessageHandler'); - break; - } - }; - opera.extension.addEventListener( "message", instance.operaMessageHandler, false); } else if (typeof(safari) !== 'undefined') { // safari instance.detectedBrowser = 'Safari'; @@ -137,7 +99,7 @@ var BabelExt = (function(Global, unsafeGlobal) { * This is necessary due to the double-layered asynchronous calls we're making. Calls to * background pages are asynchronous as it is, so when we make an asynchronous call from the * foreground page to the background, we need to hold on to a reference to our callback that lives - * in the context of the foreground, because Opera, Safari and Firefox do not allow you to pass + * in the context of the foreground, because Safari and Firefox do not allow you to pass * a foreground callback function to the background page. * * Note that this is not necessary in Chrome, because Chrome allows you to pass @@ -199,14 +161,6 @@ var BabelExt = (function(Global, unsafeGlobal) { chrome.runtime.sendMessage(thisJSON, callback); }; break; - case 'Opera': - instance.bgMessage = function(thisJSON, callback) { - if (typeof(callback) === 'function') { - thisJSON.callbackID = callbackQueue.add(callback); - } - opera.extension.postMessage(JSON.stringify(thisJSON)); - }; - break; case 'Safari': instance.bgMessage = function(thisJSON, callback) { if (typeof(callback) === 'function') { @@ -281,7 +235,7 @@ var BabelExt = (function(Global, unsafeGlobal) { /* * notifications - abstracted functions for creating notifications, though they are not natively - * supported in all browsers, so they are "faked" in Safari and Opera + * supported in all browsers, so they are "faked" in Safari * */ instance.browserNotification = { @@ -306,15 +260,6 @@ var BabelExt = (function(Global, unsafeGlobal) { }; instance.bgMessage(thisJSON); break; - case 'Opera': - thisJSON = { - requestType: 'createNotification', - icon: icon, - title: title, - text: text - }; - instance.bgMessage(thisJSON); - break; case 'Safari': thisJSON = { requestType: 'createNotification', @@ -391,7 +336,6 @@ var BabelExt = (function(Global, unsafeGlobal) { instance.bgMessage(thisJSON); callback(url); break; - case 'Opera': case 'Safari': instance.callbackQueue.callbacks[instance.callbackQueue.count] = callback; if (!instance.browserHistory.initialized) { @@ -468,8 +412,6 @@ var BabelExt = (function(Global, unsafeGlobal) { }; instance.bgMessage(thisJSON); // todo: add callback support - break; - case 'Opera': break; case 'Safari': // send a message to the background page to create this context menu @@ -520,7 +462,7 @@ var BabelExt = (function(Global, unsafeGlobal) { * - url - URL to open tab to * - focused - boolean, true = focused, false = background * - index - index to open tab at (i.e. 0th tab? nth tab?) - * note: index is not supported in Firefox or Opera, so it will be ignored + * note: index is not supported in Firefox, so it will be ignored */ create: function(url, focused, index) { instance.browserTabs.create(url, focused, index); diff --git a/lib/extension.js b/lib/extension.js index 0fffac9..853e9f0 100644 --- a/lib/extension.js +++ b/lib/extension.js @@ -15,20 +15,6 @@ (function(u) { // Any code that follows will run on document ready... - // this ugly hack is because Opera seems to run userJS on iFrames regardless of @include and @exclude directives. - // unfortunately, more sites than you'd guess use iframes - which can cause unexpected behavior if Opera goes and - // runs this script on a page it's not meant to be run - if (window!=window.top) { - return false; - } - - /* - * NOTE: Opera will run this on EVERY page! It does not have a "run only on matching domains" feature - * like Firefox, Safari and Chrome all have. If you want to restrict execution to certain sites, this - * is the place to add some code to check location.href and return false if not a match. - * - */ - /* * GREASEMONKEY COMPATIBILITY SECTION - you can remove these items if you're not porting a GM script. * diff --git a/sink.html b/sink.html index ac2637c..1bac4d9 100644 --- a/sink.html +++ b/sink.html @@ -40,9 +40,8 @@

BabelExt Kitchen Sink Demos

This demonstration requires that you have the BabelExt extension installed in your browser. You can always acquire and compile the very latest from the Github repo, but you can also download compiled versions here for each browser: From 280648bd0ee14ddd4ac376e0860002125d594560 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 25 Sep 2014 15:30:24 +0100 Subject: [PATCH 020/105] Throw error if storage.get() has no callback, ignore it in ...set() and ...remove() The existing behaviour is very confusing to a newbie, as console.log() doesn't usually go anywhere useful, so you just see a function return without doing what you expect. Throwing an error is more useful for get(), because callbacks are really required there. Inserting a no-op callback is more useful for set() and remove(), where you usually don't care. --- lib/BabelExt.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index a08b163..b8fd061 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -507,8 +507,7 @@ var BabelExt = (function(Global, unsafeGlobal) { */ get: function(key, callback) { if (typeof(callback) !== 'function') { - console.log('ERROR: no callback provided for BabelExt.storage.get()'); - return false; + throw 'ERROR: no callback provided for BabelExt.storage.get()'; } instance.browserStorage.get(key, callback); }, @@ -516,23 +515,14 @@ var BabelExt = (function(Global, unsafeGlobal) { * storage.set - sets storage for [key] to [value], calls callback with key, value as parameters */ set: function(key, value, callback) { - if (typeof(callback) !== 'function') { - console.log('ERROR: no callback provided for BabelExt.storage.set()'); - return false; - } - instance.browserStorage.set(key, value, callback); + instance.browserStorage.set(key, value, callback || function() {}); }, /* * storage.remove - deletes storage item at [key], calls callback with key as parameter */ remove: function(key, callback) { - if (typeof(callback) !== 'function') { - console.log('ERROR: no callback provided for BabelExt.storage.remove()'); - return false; - } - instance.browserStorage.remove(key, callback); + instance.browserStorage.remove(key, callback || function() {}); } - }, /* * history functions - handles adding a URL to browser history From 85ccf400e7a2227b638a646d5e7ddd15e0c964cb Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 25 Sep 2014 15:42:28 +0100 Subject: [PATCH 021/105] Add BabelExt.utils.escapeHTML --- lib/BabelExt.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index b8fd061..1fa1843 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -17,6 +17,14 @@ var BabelExt = (function(Global, unsafeGlobal) { var document = Global.document; var chrome = Global.chrome; var safari = Global.safari; + var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' + }; if (typeof(self.on) !== 'undefined') { // firefox addon SDK @@ -450,6 +458,14 @@ var BabelExt = (function(Global, unsafeGlobal) { objB[key] = objA[key]; } return objB; + }, + /* + * Escape HTML characters in a string + */ + escapeHTML: function(text) { + return String(text).replace(/[&<>"'\/]/g, function (c) { + return entityMap[c]; + }); } }, From 38baa352a868600199601f05dfc75ac21e1e3a21 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 25 Sep 2014 15:44:32 +0100 Subject: [PATCH 022/105] Add BabelExt.utils.runInEmbeddedPage --- lib/BabelExt.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 1fa1843..066a4ed 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -466,6 +466,17 @@ var BabelExt = (function(Global, unsafeGlobal) { return String(text).replace(/[&<>"'\/]/g, function (c) { return entityMap[c]; }); + }, + /* For security reasons, some scripts need to be run in the context of the embedded page + * NOTE: do not use this to inject run content loaded remotely - + * the reviewers at addons.mozilla.org will reject your code because they can't check said code. + */ + runInEmbeddedPage: function(content) { + var script = document.createElement('script'); + script.textContent = '(function() {' + content + '})();'; + document.documentElement.appendChild(script); + // remove the script so developers don't see thousands of redundant tags cluttering the DOM: + setTimeout(function() { document.documentElement.removeChild(script) }, 0 ); } }, From 033b969cc610735dad2da76e919e128455539df6 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 25 Sep 2014 15:45:14 +0100 Subject: [PATCH 023/105] Add BabelExt.utis.params --- lib/BabelExt.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 066a4ed..612c988 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -441,6 +441,11 @@ var BabelExt = (function(Global, unsafeGlobal) { } }; + instance.params = {}; + window.location.search.substr(1).replace( /([^&=]*)=([^&]*)/g, function(key, value) { + instance.params[ decodeURIComponent(key) ] = decodeURIComponent(value); + }); + return { // public interface /* * utility functions - useful functions used by BabelExt to perform certain operations... @@ -477,7 +482,8 @@ var BabelExt = (function(Global, unsafeGlobal) { document.documentElement.appendChild(script); // remove the script so developers don't see thousands of redundant tags cluttering the DOM: setTimeout(function() { document.documentElement.removeChild(script) }, 0 ); - } + }, + params: instance.params }, /* From 27ed50508857f4fcff4a5533f0e2567c1d821cdf Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 25 Sep 2014 15:51:48 +0100 Subject: [PATCH 024/105] Add BabelExt.utils.dispatch --- lib/BabelExt.js | 126 ++++++++++++++++++++++++++++++++++++++++++++++- lib/extension.js | 55 +++++++++++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 612c988..5ac6681 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -442,7 +442,7 @@ var BabelExt = (function(Global, unsafeGlobal) { }; instance.params = {}; - window.location.search.substr(1).replace( /([^&=]*)=([^&]*)/g, function(key, value) { + location.search.substr(1).replace( /([^&=]*)=([^&]*)/g, function(match, key, value) { instance.params[ decodeURIComponent(key) ] = decodeURIComponent(value); }); @@ -483,7 +483,129 @@ var BabelExt = (function(Global, unsafeGlobal) { // remove the script so developers don't see thousands of redundant tags cluttering the DOM: setTimeout(function() { document.documentElement.removeChild(script) }, 0 ); }, - params: instance.params + params: instance.params, + + /* + * If your plugin needs to perform different actions on different pages, + * You can use this to dispatch page-specific actions. + */ + dispatch: function() { + + var pathname = location.pathname, + params = instance.params, + handlers = Array.prototype.slice.call( arguments, 0 ), + stash = {}; + + // check whether a string meets the specified test(s) + function check_match( string, tests ) { + if ( tests instanceof Array ) { + for ( var n=0; n!=tests.length; ++n ) + if ( check_match(string, tests[n]) ) return true; + } else if ( typeof(tests) == 'string' ) { + return string == tests; + } else if ( typeof(tests) == "number" ) { + return string == (""+tests); + } else if ( tests instanceof RegExp ) { + return tests.test(string); + } else if ( typeof(tests) == "boolean" ) { + return tests; + } else { + throw "Only arrays, strings, numbers, RegExps and booleans are allowed in match_*"; + } + } + + function next_handler() { // check and execute the next handler + + if ( !handlers.length ) return; + var handler = handlers.shift(); + + // Check match_* to see if this handler should be executed: + if ( handler.match_pathname && !check_match(pathname, handler.match_pathname) ) + return next_handler(); + if ( handler.match_params ) { + for ( var param in handler.match_params ) + if ( handler.match_params.hasOwnProperty(param) ) + if ( !params.hasOwnProperty(param) || !check_match( params[param], handler.match_params[param] ) ) + return next_handler(); + } + + // handler should be executed, retrieve arguments + var args = [ stash, pathname, params ]; + next_handler_elements(); + + // match, wait for and retrieve elements: + function next_handler_elements() { + if ( !handler.hasOwnProperty('match_elements') ) return next_handler_storage(); + if ( !( handler.match_elements instanceof Array ) ) handler.match_elements = [ handler.match_elements ]; + + var next_match = 0, observer; + + if ( typeof( MutationObserver) != 'undefined' ) observer = new MutationObserver(observe_mutation); + else if ( typeof(WebKitMutationObserver) != 'undefined' ) observer = new WebKitMutationObserver(observe_mutation); + else throw 'ERROR: This browser does not have a MutationObserver - cannot wait for elements to exist'; + + function observe_mutation() { + while ( next_match != handler.match_elements.length ) { + var element = document.querySelector(handler.match_elements[next_match]); + if ( !element ) return; + args.push( element ); + ++next_match; + } + observer.disconnect(); + return next_handler_storage(); + } + observe_mutation(); + if ( next_match != handler.match_elements.length ) { + observer.observe(document, { childList: true, subtree: true }); + } + + } + + function next_handler_storage() { + function get(data) { + args.push(data.value); + if ( handler.pass_storage.length ) + BabelExt.storage.get(handler.pass_storage.shift(), get ); + else + next_handler_preferences(); + } + // retrieve arguments from storage: + if ( handler.hasOwnProperty('pass_storage') ) { + if ( !( handler.pass_storage instanceof Array ) ) handler.pass_storage = [ handler.pass_storage ]; + BabelExt.storage.get(handler.pass_storage.shift(), get ); + } else { + next_handler_preferences(); + } + } + + // retrieve arguments from preferences: + function next_handler_preferences() { + function get(data) { + args.push(data.value); + if ( handler.pass_preferences.length ) + BabelExt.preferences.get(handler.pass_preferences.shift(), get ); + else + next_handler_handle(); + } + if ( handler.hasOwnProperty('pass_preferences') ) { + if ( !( handler.pass_preferences instanceof Array ) ) handler.pass_preferences = [ handler.pass_preferences ]; + BabelExt.preferences.get(handler.pass_preferences.shift(), get ); + } else { + next_handler_handle(); + } + } + + // execute handler: + function next_handler_handle() { + if ( handler.callback.apply( document, args ) !== false ) + return next_handler(); + } + + } + next_handler(); + + } + }, /* diff --git a/lib/extension.js b/lib/extension.js index 853e9f0..cb86c63 100644 --- a/lib/extension.js +++ b/lib/extension.js @@ -163,5 +163,60 @@ } }); + /* + * Dispatcher + * + * If you need to run different code on different pages, + * this utility provides a convenient way to dispatch + * different handler based on page properties... + */ + BabelExt.utils.dispatch( + + { + callback: function(stash) { + console.log('this handler is called on every page'); + stash.myValue = "stashed values are available to later callbacks"; + } + }, + + { + callback: function(stash) { + console.log('handlers are called in the order they are defined - this is called second.'); + console.log(stash.myValue); + } + }, + + { + // handlers must match at least one of the rules in each of match_pathname and match_params, + // and also match every rule in match_elements. So for the rule below: + // 'mypage.html' - does not match match_params, callback will not be called + // 'nomatch.html?myparam=foo' - does not match match_pathname, callback will not be called + // 'mypage.html?myparam=foo' - matches, callback will be called + match_pathname: [ 'mypage.html', /otherpage/ ], // handler is only called on /mypage.html and pages matching the regexp /otherpage/ + match_params: { + myparam: [ 'foo', /bar/ ], // handler is only called on pages that contain a matching 'myparam' parameter + requiredparam: true, // handler is only called if 'requiredparam' is present + disallowedparam: false // handler is only called if 'disallowedparam' is not present + }, + match_elements: [ '#foo', '.bar' ], // handler is only called when both matching elements are found (see "gotcha" below) + pass_storage: [ 'foo', 'bar' ], // pass stored variables 'foo' and 'bar' to the callback + pass_preferences: [ 'baz', 'qux' ], // pass preferences 'baz' and 'qux' to the callback + callback: function( stash, pathname, params, foo, bar, baz, qux ) { + return false; // skip callbacks after this one + } + }, + + { + callback: function() { + console.log('this handler will never be called, because the previous handler returned false.'); + } + } + + ); + + // Consider putting each group of handlers in a different dispatch() call. + // BabelExt.utils.dispatch() runs commands in order and waits forever for "match_elements" to appear. + // So if an element doesn't exist on a page, it will block all later handlers in the same dispatch() + /* END KITCHEN SINK DEMO CODE */ })(); \ No newline at end of file From 922ceb3842c4ae1a7966c313143c2b1d2c064c7f Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 28 Sep 2014 02:54:43 +0100 Subject: [PATCH 025/105] Import chrome-bootstrap This library allows us to style Chrome settings like native settings --- Chrome/chrome-bootstrap.css | 719 ++++++++++++++++++++ chrome-bootstrap/.gitignore | 2 + chrome-bootstrap/LICENSE | 21 + chrome-bootstrap/README.md | 32 + chrome-bootstrap/bower.json | 28 + chrome-bootstrap/chrome-bootstrap.css | 696 +++++++++++++++++++ chrome-bootstrap/chrome-bootstrap.less | 783 ++++++++++++++++++++++ chrome-bootstrap/chrome-bootstrap.min.css | 1 + chrome-bootstrap/package.json | 16 + 9 files changed, 2298 insertions(+) create mode 100644 Chrome/chrome-bootstrap.css create mode 100644 chrome-bootstrap/.gitignore create mode 100644 chrome-bootstrap/LICENSE create mode 100644 chrome-bootstrap/README.md create mode 100644 chrome-bootstrap/bower.json create mode 100644 chrome-bootstrap/chrome-bootstrap.css create mode 100644 chrome-bootstrap/chrome-bootstrap.less create mode 100644 chrome-bootstrap/chrome-bootstrap.min.css create mode 100644 chrome-bootstrap/package.json diff --git a/Chrome/chrome-bootstrap.css b/Chrome/chrome-bootstrap.css new file mode 100644 index 0000000..da89e9d --- /dev/null +++ b/Chrome/chrome-bootstrap.css @@ -0,0 +1,719 @@ +/* + * The MIT License + * + * Copyright (c) 2012 Chrome Bootstrap authors + * + * 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. + */ +.chrome-bootstrap { + font-family: 'Segoe UI', 'Chrome Droid Sans', 'Droid Sans Fallback', 'Lucida Grande', 'Tahoma', sans-serif; + font-size: 12px; + color: #303942; + cursor: default; + margin: 0; + /* Headings + ============================================== */ + /* Layout + ============================================== */ + /* Header + ============================================== */ + /* View sections + ============================================== */ + /* Control bar + ============================================== */ + /* Pagination + ============================================== */ + /* Alert + ============================================== */ + /* Tags + ============================================== */ + /* Main menu + ============================================== */ + /* Icons + ============================================== */ + /* Highlightable list + ============================================== */ + /* Input styling + ============================================== */ + /* Focused --------------------------------- */ + /* Disabled --------------------------------- */ + /* Hovering --------------------------------- */ + /* Active --------------------------------- */ + /* Modal + ============================================== */ +} +.chrome-bootstrap a { + border: none; + color: #15C; + cursor: pointer; + text-decoration: underline; + font-weight: normal; +} +.chrome-bootstrap a:hover, +.chrome-bootstrap a:focus { + outline: none; +} +.chrome-bootstrap ul, +.chrome-bootstrap ol { + padding: 0; +} +.chrome-bootstrap li { + list-style-type: none; +} +.chrome-bootstrap dl, +.chrome-bootstrap dt, +.chrome-bootstrap dd { + margin: 0; +} +.chrome-bootstrap button { + cursor: pointer; +} +.chrome-bootstrap h1, +.chrome-bootstrap h2, +.chrome-bootstrap h3, +.chrome-bootstrap h4 { + -webkit-user-select: none; + font-weight: normal; + line-height: 1; +} +.chrome-bootstrap h1 small, +.chrome-bootstrap h2 small, +.chrome-bootstrap h3 small, +.chrome-bootstrap h4 small { + font-size: 15px; + margin: 0 10px; + color: #53637D; +} +.chrome-bootstrap h1 { + -webkit-margin-after: 1em; + -webkit-margin-before: 21px; + -webkit-margin-start: 23px; + height: 18px; + font-size: 18px; +} +.chrome-bootstrap h1 a { + color: #5C6166; + text-decoration: none; +} +.chrome-bootstrap h3 { + color: black; + font-size: 1.2em; + margin-bottom: 0.8em; +} +.chrome-bootstrap h4 { + font-size: 1em; + margin-bottom: 5px; +} +.chrome-bootstrap .frame .navigation { + height: 100%; + -webkit-margin-start: 0; + position: fixed; + -webkit-margin-end: 15px; + width: 155px; + z-index: 3; +} +.chrome-bootstrap .frame .view, +.chrome-bootstrap .frame .content { + width: 738px; + overflow-x: hidden; +} +.chrome-bootstrap .frame .content { + padding-top: 55px; +} +.chrome-bootstrap .frame .content p { + text-align: justify; +} +.chrome-bootstrap .frame .with_controls .content { + padding-top: 104px; +} +.chrome-bootstrap .frame .view { + -webkit-margin-start: 155px; +} +.chrome-bootstrap .frame .view a { + font: inherit; +} +.chrome-bootstrap .frame .mainview > * { + -webkit-margin-start: -20px; + -webkit-transition: margin 100ms, opacity 100ms; + opacity: 0; + z-index: 0; + position: absolute; + top: 0; + display: block; +} +.chrome-bootstrap .frame .mainview > .selected { + -webkit-margin-start: 0; + -webkit-transition: margin 200ms, opacity 200ms; + -webkit-transition-delay: 100ms; + z-index: 1; + opacity: 1; +} +.chrome-bootstrap header { + position: fixed; + background-image: -webkit-linear-gradient(#ffffff, #ffffff 40%, rgba(255, 255, 255, 0.92)); + width: 738px; + z-index: 2; +} +.chrome-bootstrap header h1 { + padding: 21px 0 13px; + margin: 0; + border-bottom: 1px solid #EEE; +} +.chrome-bootstrap header .corner { + position: absolute; + right: 0px; + top: 21px; +} +.chrome-bootstrap header .corner input[type="text"] { + width: 210px; +} +.chrome-bootstrap header .corner.cancelable .delete { + opacity: 1; + top: 4px; + right: 5px; +} +.chrome-bootstrap section { + -webkit-padding-start: 18px; + margin-bottom: 24px; + margin-top: 8px; + max-width: 600px; +} +.chrome-bootstrap section h3 { + -webkit-margin-start: -18px; +} +.chrome-bootstrap section .row { + display: block; + margin: 0.65em 0; +} +.chrome-bootstrap .controls { + -webkit-padding-end: 3px; + -webkit-padding-start: 4px; + -webkit-transition: padding 100ms, height 100ms, opacity 100ms; + border-bottom: 1px solid #EEE; + display: -webkit-box; + overflow: hidden; + padding: 13px 0; + position: relative; +} +.chrome-bootstrap .controls .text { + display: inline-block; + margin-top: 4px; +} +.chrome-bootstrap .controls .spacer { + -webkit-box-flex: 1; +} +.chrome-bootstrap ol.pagination li { + margin: 0 2px; + display: inline-block; + line-height: 25px; +} +.chrome-bootstrap ol.pagination a { + width: 25px; + height: 24px; + text-align: center; + display: block; + background: #F0F6FE; + text-decoration: none; +} +.chrome-bootstrap ol.pagination a:hover, +.chrome-bootstrap ol.pagination a.selected { + background: #8AAAED; + color: #FFF; +} +.chrome-bootstrap .alert { + border-radius: 3px; + background: rgba(147, 184, 252, 0.2); + display: block; + position: relative; + padding: 10px 30px 10px 10px; + line-height: 17px; +} +.chrome-bootstrap .alert .delete { + top: 5px; + right: 6px; + opacity: 1; +} +.chrome-bootstrap ul.tags li { + background: #8AAAED; + color: #FFF; + border-radius: 3px; + position: relative; + display: inline-block; + padding: 2px 5px; +} +.chrome-bootstrap ul.tags li a { + color: #FFF; + text-decoration: none; +} +.chrome-bootstrap ul.tags li a:hover { + text-decoration: underline; +} +.chrome-bootstrap ul.tags li .delete { + opacity: 1; + position: relative; + display: inline-block; + width: 13px; + height: 12px; + top: 1px; + background-position-y: -1px; +} +.chrome-bootstrap ul.menu { + -webkit-margin-before: 1em; + -webkit-margin-after: 2em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + -webkit-padding-start: 40px; + list-style-type: none; + padding: 0; +} +.chrome-bootstrap ul.menu li { + -webkit-border-start: 6px solid transparent; + -webkit-padding-start: 18px; + -webkit-user-select: none; + display: list-item; + text-align: -webkit-match-parent; +} +.chrome-bootstrap ul.menu li.selected { + -webkit-border-start-color: #4e5764; +} +.chrome-bootstrap ul.menu li.selected a { + color: #464E5A; +} +.chrome-bootstrap ul.menu li a { + border: 0; + color: #999; + cursor: pointer; + font: inherit; + line-height: 29px; + margin: 0; + padding: 0; + text-decoration: none; + display: block; +} +.chrome-bootstrap .arrow_collapse { + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 6px solid #999; + -webkit-margin-end: 4px; + top: 1px; +} +.chrome-bootstrap .arrow_expand { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 7px solid #999; + -webkit-margin-end: 4px; +} +.chrome-bootstrap .arrow { + width: 0; + height: 0; + position: relative; + display: inline-block; +} +.chrome-bootstrap .delete { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg=="); + background-repeat: no-repeat; + display: block; + opacity: 0; + height: 14px; + width: 14px; + -webkit-transition: 150ms opacity; + background-color: transparent; + text-indent: -5000px; + position: absolute; +} +.chrome-bootstrap .delete:hover { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII="); +} +.chrome-bootstrap .highlightable li { + position: relative; + padding: 2px 0; +} +.chrome-bootstrap .highlightable li:hover > a:not(.action), +.chrome-bootstrap .highlightable li a:not(.action):focus { + background-color: #F0F6FE; + color: #555; +} +.chrome-bootstrap .highlightable li:hover > .action { + opacity: 0.7; +} +.chrome-bootstrap .highlightable li a { + padding: 5px; + display: block; + position: relative; + z-index: 0; + text-decoration: none; +} +.chrome-bootstrap .highlightable li dt { + font-size: 105%; + margin-bottom: 3px; +} +.chrome-bootstrap .highlightable li dd { + color: #999; + overflow: hidden; + white-space: nowrap; + font-size: 10px; + margin-top: 5px; +} +.chrome-bootstrap .highlightable li .tags { + float: left; + margin-top: -1px; + font-size: 12px; +} +.chrome-bootstrap .highlightable li .tags li:last-child { + margin-right: 5px; +} +.chrome-bootstrap .highlightable li .tags li:hover > a:not(.action) { + background: #8AAAED; + color: #FFF; +} +.chrome-bootstrap .highlightable li .tags li a { + padding: 0; +} +.chrome-bootstrap .highlightable li .action { + -webkit-appearance: none; + -webkit-transition: opacity 150ms; + background: #8AAAED; + border: none; + border-radius: 2px; + color: white; + opacity: 0; + margin-top: 0; + font-size: 10px; + padding: 1px 6px; + position: absolute; + top: 8px; + right: 32px; + -webkit-transition: 150ms opacity; + cursor: pointer; +} +.chrome-bootstrap .highlightable li .action:hover { + opacity: 1; +} +.chrome-bootstrap .highlightable li .highlightable { + -webkit-margin-start: 30px; +} +.chrome-bootstrap .highlightable.editable .delete { + position: absolute; + top: 7px; + right: 5px; +} +.chrome-bootstrap .highlightable.editable li:hover > .delete { + opacity: 1; +} +.chrome-bootstrap .highlightable.draggable .handle { + width: 8px; + height: 41px; + background-image: linear-gradient(to bottom, #c1c1c1 50%, rgba(255, 255, 255, 0) 0%); + background-position: center; + background-size: 100% 17%; + background-repeat: repeat-y; + visibility: hidden; + position: absolute; + top: 4px; + left: 2px; +} +.chrome-bootstrap .highlightable.draggable .handle:hover { + cursor: move; + cursor: -webkit-grab; + display: block; +} +.chrome-bootstrap .highlightable.draggable .handle:after { + margin-left: 3px; + width: 2px; + height: 41px; + background: #F0F6FE; + content: ""; + display: block; +} +.chrome-bootstrap .highlightable.draggable li:hover .handle { + visibility: visible; + z-index: 1; +} +.chrome-bootstrap .highlightable.draggable li > .item { + padding-left: 20px; +} +.chrome-bootstrap .match { + background: #f2f37b; + display: inline-block; + margin: 0 1px; +} +.chrome-bootstrap select, +.chrome-bootstrap input[type='checkbox'], +.chrome-bootstrap input[type='radio'], +.chrome-bootstrap input[type='button'], +.chrome-bootstrap button { + -webkit-appearance: none; + -webkit-user-select: none; + background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 2px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #444; + font: inherit; + margin: 0 1px 0 0; + text-shadow: 0 1px 0 #F0F0F0; +} +.chrome-bootstrap button.small { + padding: 1px 5px 2px; + min-height: 1em; +} +.chrome-bootstrap input[type='checkbox']:checked::before { + -webkit-user-select: none; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wDBhYcG79aGIsAAACbSURBVBjTjdFBCkFhFAXgj4fp24PBy0SZ2ICRXRgYGb2xlKzBSEo2YgsiKWVoZgFKMjD5X/2Ux6lb99bpnNO5lKMR5i8MsEQHkhJiEzlS9HCqfiFWMUIt3AfsC3KKLCL30Qr7HfM4Ro4h6rhiEqmusIMKuphGqo+ogSPGcbYLzh91vdkXSHDDBk+0gxussS3rNcMCs+D6E18/9gLPPhbDshfzLgAAAABJRU5ErkJggg=="); + background-size: 100% 100%; + content: ''; + display: block; + height: 100%; + width: 100%; +} +.chrome-bootstrap html[dir='rtl'] input[type='checkbox']:checked::before { + -webkit-transform: scaleX(-1); +} +.chrome-bootstrap input[type='radio']:checked::before { + background-color: #666; + border-radius: 100%; + bottom: 3px; + content: ''; + display: block; + left: 3px; + position: absolute; + right: 3px; + top: 3px; +} +.chrome-bootstrap select { + -webkit-appearance: none; + -webkit-padding-end: 20px; + -webkit-padding-start: 6px; + /* OVERRIDE */ + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC), -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); + background-position: right center; + background-repeat: no-repeat; +} +.chrome-bootstrap select { + min-height: 2em; + min-width: 4em; +} +.chrome-bootstrap html[dir='rtl'] select { + background-position: center left; +} +.chrome-bootstrap input[type='checkbox'] { + bottom: 2px; + height: 13px; + position: relative; + vertical-align: middle; + width: 13px; +} +.chrome-bootstrap input[type='radio'] { + /* OVERRIDE */ + border-radius: 100%; + bottom: 3px; + height: 15px; + position: relative; + vertical-align: middle; + width: 15px; +} +.chrome-bootstrap button { + -webkit-padding-end: 10px; + -webkit-padding-start: 10px; + min-height: 2em; + min-width: 4em; +} +.chrome-bootstrap input[type='text'], +.chrome-bootstrap input[type='number'], +.chrome-bootstrap input[type='search'] { + border: 1px solid #BFBFBF; + border-radius: 2px; + box-sizing: border-box; + color: #444; + font: inherit; + margin: 0; + min-height: 2em; + padding: 3px; + padding-bottom: 4px; +} +.chrome-bootstrap .radio, +.chrome-bootstrap .checkbox { + margin: 0.65em 0; +} +.chrome-bootstrap select:focus, +.chrome-bootstrap input[type='checkbox']:focus, +.chrome-bootstrap input[type='password']:focus, +.chrome-bootstrap input[type='radio']:focus, +.chrome-bootstrap input[type='search']:focus, +.chrome-bootstrap input[type='text']:focus, +.chrome-bootstrap input[type='number']:focus, +.chrome-bootstrap button:focus { + /* OVERRIDE */ + -webkit-transition: border-color 200ms; + /* We use border color because it follows the border radius (unlike outline). + * This is particularly noticeable on mac. */ + border-color: #4d90fe; + outline: none; +} +.chrome-bootstrap button:disabled, +.chrome-bootstrap select:disabled { + background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); + border-color: rgba(80, 80, 80, 0.2); + box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #aaa; + cursor: default; +} +.chrome-bootstrap select:disabled { + /* OVERRIDE */ + background-image: -webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAAXNSR0IArs4c6QAAAAd0SU1FB9sLAxYEBKriBmwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAABLSURBVCiRY2CgA4gC4jQ8OIpokxKBoKGh4T8uDJIn2rD///8rLFiwYCE2g0DiIHkSfIndQLIMwmYgRQYhG/j27dsmig1CMpCVGHUAo8FcsHfxfXQAAAAASUVORK5CYII=") 1x), -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); +} +.chrome-bootstrap input[type='checkbox']:disabled, +.chrome-bootstrap input[type='radio']:disabled { + opacity: .75; +} +.chrome-bootstrap input[type='search']:disabled, +.chrome-bootstrap input[type='number']:disabled, +.chrome-bootstrap input[type='text']:disabled { + color: #999; +} +.chrome-bootstrap select:hover:enabled, +.chrome-bootstrap input[type='checkbox']:hover:enabled, +.chrome-bootstrap input[type='radio']:hover:enabled, +.chrome-bootstrap button:hover:enabled { + background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); + border-color: rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95); + color: black; +} +.chrome-bootstrap select:hover:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); +} +.chrome-bootstrap select:active:enabled, +.chrome-bootstrap input[type='checkbox']:active:enabled, +.chrome-bootstrap input[type='radio']:active:enabled, +.chrome-bootstrap button:active:enabled { + background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); + box-shadow: none; + text-shadow: none; +} +.chrome-bootstrap select:active:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); +} +.chrome-bootstrap .overlay { + -webkit-box-align: center; + -webkit-box-orient: vertical; + -webkit-box-pack: center; + -webkit-transition: opacity .2s; + background-color: rgba(255, 255, 255, 0.75); + bottom: 0; + display: -webkit-box; + left: 0; + overflow: auto; + padding: 20px; + position: fixed; + right: 0; + top: 0; + z-index: 5; + opacity: 1; +} +.chrome-bootstrap .overlay.transparent { + opacity: 0; +} +.chrome-bootstrap .overlay.transparent .page { + -webkit-transform: scale(0.99) translateY(-20px); +} +.chrome-bootstrap .overlay .page { + -webkit-border-radius: 3px; + -webkit-box-orient: vertical; + -webkit-transition: 200ms -webkit-transform; + background: white; + box-shadow: 0 4px 23px 5px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0, 0, 0, 0.15); + color: #333; + display: -webkit-box; + min-width: 400px; + padding: 0; + position: relative; + overflow: hidden; +} +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(1); + } + 40% { + -webkit-transform: scale(1.02); + } + 60% { + -webkit-transform: scale(1.02); + } + 100% { + -webkit-transform: scale(1); + } +} +.chrome-bootstrap .overlay .page.pulse { + -webkit-animation-duration: 180ms; + -webkit-animation-iteration-count: 1; + -webkit-animation-name: pulse; + -webkit-animation-timing-function: ease-in-out; +} +.chrome-bootstrap .overlay .page h1 { + -webkit-padding-end: 24px; + -webkit-user-select: none; + color: #333; + font-size: 120%; + margin: 0; + padding: 14px 17px 14px; + text-shadow: white 0 1px 2px; +} +.chrome-bootstrap .overlay .page ul li { + padding: 5px 0; +} +.chrome-bootstrap .overlay .page ul.tags li { + padding: 2px 5px; +} +.chrome-bootstrap .overlay .page .content-area { + -webkit-box-flex: 1; + overflow: auto; + padding: 6px 17px 6px; +} +.chrome-bootstrap .overlay .page .close-button { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAUklEQVR4XqXPYQrAIAhAYW/gXd8NJxTopVqsGEhtf+L9/ERU2k/HSMFQpKcYJeNFI9Be0LCMij8cYyjj5EHIivGBkwLfrbX3IF8PqumVmnDpEG+eDsKibPG2JwAAAABJRU5ErkJggg=='); + background-position: center; + background-repeat: no-repeat; + height: 14px; + position: absolute; + right: 7px; + top: 7px; + width: 14px; +} +.chrome-bootstrap .overlay .page .close-button:hover { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAnUlEQVR4XoWQQQ6CQAxFewjkJkMCyXgJPMk7AiYczyBeZEAX6AKctGIaN+bt+trk9wtGQc/IkhnoKGxqqiWxOSZalapWFZ6VrIUDExsN0a5JRBq9LoVOR0eEQMoEhKizXhhsn0p1sCWVo7CwOf1RytPL8CPvwuBUoHL6ugeK30CVD1TqK7V/hdpe+VNChhOzV8xWny/+xosHF8578W/Hmc1OOC3wmwAAAABJRU5ErkJggg=='); +} +.chrome-bootstrap .overlay .page .action-area { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: end; + display: -webkit-box; + padding: 14px 17px; +} +.chrome-bootstrap .overlay .page .action-area-right { + display: -webkit-box; +} +.chrome-bootstrap .overlay .page .button-strip { + -webkit-box-orient: horizontal; + display: -webkit-box; +} +.chrome-bootstrap .overlay .page .button-strip button { + -webkit-margin-start: 10px; + display: block; +} diff --git a/chrome-bootstrap/.gitignore b/chrome-bootstrap/.gitignore new file mode 100644 index 0000000..2752eb9 --- /dev/null +++ b/chrome-bootstrap/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.DS_Store diff --git a/chrome-bootstrap/LICENSE b/chrome-bootstrap/LICENSE new file mode 100644 index 0000000..20219d5 --- /dev/null +++ b/chrome-bootstrap/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2012 Chrome Bootstrap authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/chrome-bootstrap/README.md b/chrome-bootstrap/README.md new file mode 100644 index 0000000..0f6bfc1 --- /dev/null +++ b/chrome-bootstrap/README.md @@ -0,0 +1,32 @@ +Chrome UI bootstrap +================ + +[![NPM version](https://badge.fury.io/js/chrome-bootstrap.svg)](http://badge.fury.io/js/chrome-bootstrap) +[![Bower version](https://badge.fury.io/bo/chrome-bootstrap.svg)](http://badge.fury.io/bo/chrome-bootstrap) + +[Style guide](http://roykolak.github.com/chrome-bootstrap/) + +Chrome's UI stylings look pretty nice and are advancing quickly. In order to make top quanlity extensions, apps, and other one offs that look and feel like Chrome's system apps, these styles must be exposed for easy reuse. + +This project aims at accomplishing this goal while keeping the scope extremely simple. + +Scope +---------------- + +* Reusable selectors +* CSS animations +* I18n'ed properties +* Images encoded as strings + +And that's it. Let's keep it simple! + +Setup +--------------- + +Chrome bootstrap is packaged as a npm package so run the following to install dependencies + + $ npm install + +To compile css with each less change, use the start command + + $ npm start diff --git a/chrome-bootstrap/bower.json b/chrome-bootstrap/bower.json new file mode 100644 index 0000000..3bd1f4b --- /dev/null +++ b/chrome-bootstrap/bower.json @@ -0,0 +1,28 @@ +{ + "name": "chrome-bootstrap", + "version": "1.4.0", + "homepage": "https://github.com/roykolak/chrome-bootstrap", + "authors": [ + "Roy Kolak " + ], + "description": "Reusable Chrome style settings UI", + "main": "chrome-bootstrap.css", + "keywords": [ + "css", + "bootstrap", + "chrome", + "extensions", + "ui", + "interface", + "less", + "google" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/chrome-bootstrap/chrome-bootstrap.css b/chrome-bootstrap/chrome-bootstrap.css new file mode 100644 index 0000000..3ae66d5 --- /dev/null +++ b/chrome-bootstrap/chrome-bootstrap.css @@ -0,0 +1,696 @@ +.chrome-bootstrap { + font-family: 'Segoe UI', 'Chrome Droid Sans', 'Droid Sans Fallback', 'Lucida Grande', 'Tahoma', sans-serif; + font-size: 12px; + color: #303942; + cursor: default; + margin: 0; + /* Headings + ============================================== */ + /* Layout + ============================================== */ + /* Header + ============================================== */ + /* View sections + ============================================== */ + /* Control bar + ============================================== */ + /* Pagination + ============================================== */ + /* Alert + ============================================== */ + /* Tags + ============================================== */ + /* Main menu + ============================================== */ + /* Icons + ============================================== */ + /* Highlightable list + ============================================== */ + /* Input styling + ============================================== */ + /* Focused --------------------------------- */ + /* Disabled --------------------------------- */ + /* Hovering --------------------------------- */ + /* Active --------------------------------- */ + /* Modal + ============================================== */ +} +.chrome-bootstrap a { + border: none; + color: #15C; + cursor: pointer; + text-decoration: underline; + font-weight: normal; +} +.chrome-bootstrap a:hover, +.chrome-bootstrap a:focus { + outline: none; +} +.chrome-bootstrap ul, +.chrome-bootstrap ol { + padding: 0; +} +.chrome-bootstrap li { + list-style-type: none; +} +.chrome-bootstrap dl, +.chrome-bootstrap dt, +.chrome-bootstrap dd { + margin: 0; +} +.chrome-bootstrap button { + cursor: pointer; +} +.chrome-bootstrap h1, +.chrome-bootstrap h2, +.chrome-bootstrap h3, +.chrome-bootstrap h4 { + -webkit-user-select: none; + font-weight: normal; + line-height: 1; +} +.chrome-bootstrap h1 small, +.chrome-bootstrap h2 small, +.chrome-bootstrap h3 small, +.chrome-bootstrap h4 small { + font-size: 15px; + margin: 0 10px; + color: #53637D; +} +.chrome-bootstrap h1 { + -webkit-margin-after: 1em; + -webkit-margin-before: 21px; + -webkit-margin-start: 23px; + height: 18px; + font-size: 18px; +} +.chrome-bootstrap h1 a { + color: #5C6166; + text-decoration: none; +} +.chrome-bootstrap h3 { + color: black; + font-size: 1.2em; + margin-bottom: 0.8em; +} +.chrome-bootstrap h4 { + font-size: 1em; + margin-bottom: 5px; +} +.chrome-bootstrap .frame .navigation { + height: 100%; + -webkit-margin-start: 0; + position: fixed; + -webkit-margin-end: 15px; + width: 155px; + z-index: 3; +} +.chrome-bootstrap .frame .view, +.chrome-bootstrap .frame .content { + width: 738px; + overflow-x: hidden; +} +.chrome-bootstrap .frame .content { + padding-top: 55px; +} +.chrome-bootstrap .frame .content p { + text-align: justify; +} +.chrome-bootstrap .frame .with_controls .content { + padding-top: 104px; +} +.chrome-bootstrap .frame .view { + -webkit-margin-start: 155px; +} +.chrome-bootstrap .frame .view a { + font: inherit; +} +.chrome-bootstrap .frame .mainview > * { + -webkit-margin-start: -20px; + -webkit-transition: margin 100ms, opacity 100ms; + opacity: 0; + z-index: 0; + position: absolute; + top: 0; + display: block; +} +.chrome-bootstrap .frame .mainview > .selected { + -webkit-margin-start: 0; + -webkit-transition: margin 200ms, opacity 200ms; + -webkit-transition-delay: 100ms; + z-index: 1; + opacity: 1; +} +.chrome-bootstrap header { + position: fixed; + background-image: -webkit-linear-gradient(#ffffff, #ffffff 40%, rgba(255, 255, 255, 0.92)); + width: 738px; + z-index: 2; +} +.chrome-bootstrap header h1 { + padding: 21px 0 13px; + margin: 0; + border-bottom: 1px solid #EEE; +} +.chrome-bootstrap header .corner { + position: absolute; + right: 0px; + top: 21px; +} +.chrome-bootstrap header .corner input[type="text"] { + width: 210px; +} +.chrome-bootstrap header .corner.cancelable .delete { + opacity: 1; + top: 4px; + right: 5px; +} +.chrome-bootstrap section { + -webkit-padding-start: 18px; + margin-bottom: 24px; + margin-top: 8px; + max-width: 600px; +} +.chrome-bootstrap section h3 { + -webkit-margin-start: -18px; +} +.chrome-bootstrap section .row { + display: block; + margin: 0.65em 0; +} +.chrome-bootstrap .controls { + -webkit-padding-end: 3px; + -webkit-padding-start: 4px; + -webkit-transition: padding 100ms, height 100ms, opacity 100ms; + border-bottom: 1px solid #EEE; + display: -webkit-box; + overflow: hidden; + padding: 13px 0; + position: relative; +} +.chrome-bootstrap .controls .text { + display: inline-block; + margin-top: 4px; +} +.chrome-bootstrap .controls .spacer { + -webkit-box-flex: 1; +} +.chrome-bootstrap ol.pagination li { + margin: 0 2px; + display: inline-block; + line-height: 25px; +} +.chrome-bootstrap ol.pagination a { + width: 25px; + height: 24px; + text-align: center; + display: block; + background: #F0F6FE; + text-decoration: none; +} +.chrome-bootstrap ol.pagination a:hover, +.chrome-bootstrap ol.pagination a.selected { + background: #8AAAED; + color: #FFF; +} +.chrome-bootstrap .alert { + border-radius: 3px; + background: rgba(147, 184, 252, 0.2); + display: block; + position: relative; + padding: 10px 30px 10px 10px; + line-height: 17px; +} +.chrome-bootstrap .alert .delete { + top: 5px; + right: 6px; + opacity: 1; +} +.chrome-bootstrap ul.tags li { + background: #8AAAED; + color: #FFF; + border-radius: 3px; + position: relative; + display: inline-block; + padding: 2px 5px; +} +.chrome-bootstrap ul.tags li a { + color: #FFF; + text-decoration: none; +} +.chrome-bootstrap ul.tags li a:hover { + text-decoration: underline; +} +.chrome-bootstrap ul.tags li .delete { + opacity: 1; + position: relative; + display: inline-block; + width: 13px; + height: 12px; + top: 1px; + background-position-y: -1px; +} +.chrome-bootstrap ul.menu { + -webkit-margin-before: 1em; + -webkit-margin-after: 2em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + -webkit-padding-start: 40px; + list-style-type: none; + padding: 0; +} +.chrome-bootstrap ul.menu li { + -webkit-border-start: 6px solid transparent; + -webkit-padding-start: 18px; + -webkit-user-select: none; + display: list-item; + text-align: -webkit-match-parent; +} +.chrome-bootstrap ul.menu li.selected { + -webkit-border-start-color: #4e5764; +} +.chrome-bootstrap ul.menu li.selected a { + color: #464E5A; +} +.chrome-bootstrap ul.menu li a { + border: 0; + color: #999; + cursor: pointer; + font: inherit; + line-height: 29px; + margin: 0; + padding: 0; + text-decoration: none; + display: block; +} +.chrome-bootstrap .arrow_collapse { + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 6px solid #999; + -webkit-margin-end: 4px; + top: 1px; +} +.chrome-bootstrap .arrow_expand { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 7px solid #999; + -webkit-margin-end: 4px; +} +.chrome-bootstrap .arrow { + width: 0; + height: 0; + position: relative; + display: inline-block; +} +.chrome-bootstrap .delete { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg=="); + background-repeat: no-repeat; + display: block; + opacity: 0; + height: 14px; + width: 14px; + -webkit-transition: 150ms opacity; + background-color: transparent; + text-indent: -5000px; + position: absolute; +} +.chrome-bootstrap .delete:hover { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII="); +} +.chrome-bootstrap .highlightable li { + position: relative; + padding: 2px 0; +} +.chrome-bootstrap .highlightable li:hover > a:not(.action), +.chrome-bootstrap .highlightable li a:not(.action):focus { + background-color: #F0F6FE; + color: #555; +} +.chrome-bootstrap .highlightable li:hover > .action { + opacity: 0.7; +} +.chrome-bootstrap .highlightable li a { + padding: 5px; + display: block; + position: relative; + z-index: 0; + text-decoration: none; +} +.chrome-bootstrap .highlightable li dt { + font-size: 105%; + margin-bottom: 3px; +} +.chrome-bootstrap .highlightable li dd { + color: #999; + overflow: hidden; + white-space: nowrap; + font-size: 10px; + margin-top: 5px; +} +.chrome-bootstrap .highlightable li .tags { + float: left; + margin-top: -1px; + font-size: 12px; +} +.chrome-bootstrap .highlightable li .tags li:last-child { + margin-right: 5px; +} +.chrome-bootstrap .highlightable li .tags li:hover > a:not(.action) { + background: #8AAAED; + color: #FFF; +} +.chrome-bootstrap .highlightable li .tags li a { + padding: 0; +} +.chrome-bootstrap .highlightable li .action { + -webkit-appearance: none; + -webkit-transition: opacity 150ms; + background: #8AAAED; + border: none; + border-radius: 2px; + color: white; + opacity: 0; + margin-top: 0; + font-size: 10px; + padding: 1px 6px; + position: absolute; + top: 8px; + right: 32px; + -webkit-transition: 150ms opacity; + cursor: pointer; +} +.chrome-bootstrap .highlightable li .action:hover { + opacity: 1; +} +.chrome-bootstrap .highlightable li .highlightable { + -webkit-margin-start: 30px; +} +.chrome-bootstrap .highlightable.editable .delete { + position: absolute; + top: 7px; + right: 5px; +} +.chrome-bootstrap .highlightable.editable li:hover > .delete { + opacity: 1; +} +.chrome-bootstrap .highlightable.draggable .handle { + width: 8px; + height: 41px; + background-image: linear-gradient(to bottom, #c1c1c1 50%, rgba(255, 255, 255, 0) 0%); + background-position: center; + background-size: 100% 17%; + background-repeat: repeat-y; + visibility: hidden; + position: absolute; + top: 4px; + left: 2px; +} +.chrome-bootstrap .highlightable.draggable .handle:hover { + cursor: move; + cursor: -webkit-grab; + display: block; +} +.chrome-bootstrap .highlightable.draggable .handle:after { + margin-left: 3px; + width: 2px; + height: 41px; + background: #F0F6FE; + content: ""; + display: block; +} +.chrome-bootstrap .highlightable.draggable li:hover .handle { + visibility: visible; + z-index: 1; +} +.chrome-bootstrap .highlightable.draggable li > .item { + padding-left: 20px; +} +.chrome-bootstrap .match { + background: #f2f37b; + display: inline-block; + margin: 0 1px; +} +.chrome-bootstrap select, +.chrome-bootstrap input[type='checkbox'], +.chrome-bootstrap input[type='radio'], +.chrome-bootstrap input[type='button'], +.chrome-bootstrap button { + -webkit-appearance: none; + -webkit-user-select: none; + background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 2px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #444; + font: inherit; + margin: 0 1px 0 0; + text-shadow: 0 1px 0 #F0F0F0; +} +.chrome-bootstrap button.small { + padding: 1px 5px 2px; + min-height: 1em; +} +.chrome-bootstrap input[type='checkbox']:checked::before { + -webkit-user-select: none; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wDBhYcG79aGIsAAACbSURBVBjTjdFBCkFhFAXgj4fp24PBy0SZ2ICRXRgYGb2xlKzBSEo2YgsiKWVoZgFKMjD5X/2Ux6lb99bpnNO5lKMR5i8MsEQHkhJiEzlS9HCqfiFWMUIt3AfsC3KKLCL30Qr7HfM4Ro4h6rhiEqmusIMKuphGqo+ogSPGcbYLzh91vdkXSHDDBk+0gxussS3rNcMCs+D6E18/9gLPPhbDshfzLgAAAABJRU5ErkJggg=="); + background-size: 100% 100%; + content: ''; + display: block; + height: 100%; + width: 100%; +} +.chrome-bootstrap html[dir='rtl'] input[type='checkbox']:checked::before { + -webkit-transform: scaleX(-1); +} +.chrome-bootstrap input[type='radio']:checked::before { + background-color: #666; + border-radius: 100%; + bottom: 3px; + content: ''; + display: block; + left: 3px; + position: absolute; + right: 3px; + top: 3px; +} +.chrome-bootstrap select { + -webkit-appearance: none; + -webkit-padding-end: 20px; + -webkit-padding-start: 6px; + /* OVERRIDE */ + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC), -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); + background-position: right center; + background-repeat: no-repeat; +} +.chrome-bootstrap select { + min-height: 2em; + min-width: 4em; +} +.chrome-bootstrap html[dir='rtl'] select { + background-position: center left; +} +.chrome-bootstrap input[type='checkbox'] { + bottom: 2px; + height: 13px; + position: relative; + vertical-align: middle; + width: 13px; +} +.chrome-bootstrap input[type='radio'] { + /* OVERRIDE */ + border-radius: 100%; + bottom: 3px; + height: 15px; + position: relative; + vertical-align: middle; + width: 15px; +} +.chrome-bootstrap button { + -webkit-padding-end: 10px; + -webkit-padding-start: 10px; + min-height: 2em; + min-width: 4em; +} +.chrome-bootstrap input[type='text'], +.chrome-bootstrap input[type='number'], +.chrome-bootstrap input[type='search'] { + border: 1px solid #BFBFBF; + border-radius: 2px; + box-sizing: border-box; + color: #444; + font: inherit; + margin: 0; + min-height: 2em; + padding: 3px; + padding-bottom: 4px; +} +.chrome-bootstrap .radio, +.chrome-bootstrap .checkbox { + margin: 0.65em 0; +} +.chrome-bootstrap select:focus, +.chrome-bootstrap input[type='checkbox']:focus, +.chrome-bootstrap input[type='password']:focus, +.chrome-bootstrap input[type='radio']:focus, +.chrome-bootstrap input[type='search']:focus, +.chrome-bootstrap input[type='text']:focus, +.chrome-bootstrap input[type='number']:focus, +.chrome-bootstrap button:focus { + /* OVERRIDE */ + -webkit-transition: border-color 200ms; + /* We use border color because it follows the border radius (unlike outline). + * This is particularly noticeable on mac. */ + border-color: #4d90fe; + outline: none; +} +.chrome-bootstrap button:disabled, +.chrome-bootstrap select:disabled { + background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); + border-color: rgba(80, 80, 80, 0.2); + box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #aaa; + cursor: default; +} +.chrome-bootstrap select:disabled { + /* OVERRIDE */ + background-image: -webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAAXNSR0IArs4c6QAAAAd0SU1FB9sLAxYEBKriBmwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAABLSURBVCiRY2CgA4gC4jQ8OIpokxKBoKGh4T8uDJIn2rD///8rLFiwYCE2g0DiIHkSfIndQLIMwmYgRQYhG/j27dsmig1CMpCVGHUAo8FcsHfxfXQAAAAASUVORK5CYII=") 1x), -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); +} +.chrome-bootstrap input[type='checkbox']:disabled, +.chrome-bootstrap input[type='radio']:disabled { + opacity: .75; +} +.chrome-bootstrap input[type='search']:disabled, +.chrome-bootstrap input[type='number']:disabled, +.chrome-bootstrap input[type='text']:disabled { + color: #999; +} +.chrome-bootstrap select:hover:enabled, +.chrome-bootstrap input[type='checkbox']:hover:enabled, +.chrome-bootstrap input[type='radio']:hover:enabled, +.chrome-bootstrap button:hover:enabled { + background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); + border-color: rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95); + color: black; +} +.chrome-bootstrap select:hover:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); +} +.chrome-bootstrap select:active:enabled, +.chrome-bootstrap input[type='checkbox']:active:enabled, +.chrome-bootstrap input[type='radio']:active:enabled, +.chrome-bootstrap button:active:enabled { + background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); + box-shadow: none; + text-shadow: none; +} +.chrome-bootstrap select:active:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); +} +.chrome-bootstrap .overlay { + -webkit-box-align: center; + -webkit-box-orient: vertical; + -webkit-box-pack: center; + -webkit-transition: opacity .2s; + background-color: rgba(255, 255, 255, 0.75); + bottom: 0; + display: -webkit-box; + left: 0; + overflow: auto; + padding: 20px; + position: fixed; + right: 0; + top: 0; + z-index: 5; + opacity: 1; +} +.chrome-bootstrap .overlay.transparent { + opacity: 0; +} +.chrome-bootstrap .overlay.transparent .page { + -webkit-transform: scale(0.99) translateY(-20px); +} +.chrome-bootstrap .overlay .page { + -webkit-border-radius: 3px; + -webkit-box-orient: vertical; + -webkit-transition: 200ms -webkit-transform; + background: white; + box-shadow: 0 4px 23px 5px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0, 0, 0, 0.15); + color: #333; + display: -webkit-box; + min-width: 400px; + padding: 0; + position: relative; + overflow: hidden; +} +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(1); + } + 40% { + -webkit-transform: scale(1.02); + } + 60% { + -webkit-transform: scale(1.02); + } + 100% { + -webkit-transform: scale(1); + } +} +.chrome-bootstrap .overlay .page.pulse { + -webkit-animation-duration: 180ms; + -webkit-animation-iteration-count: 1; + -webkit-animation-name: pulse; + -webkit-animation-timing-function: ease-in-out; +} +.chrome-bootstrap .overlay .page h1 { + -webkit-padding-end: 24px; + -webkit-user-select: none; + color: #333; + font-size: 120%; + margin: 0; + padding: 14px 17px 14px; + text-shadow: white 0 1px 2px; +} +.chrome-bootstrap .overlay .page ul li { + padding: 5px 0; +} +.chrome-bootstrap .overlay .page ul.tags li { + padding: 2px 5px; +} +.chrome-bootstrap .overlay .page .content-area { + -webkit-box-flex: 1; + overflow: auto; + padding: 6px 17px 6px; +} +.chrome-bootstrap .overlay .page .close-button { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAUklEQVR4XqXPYQrAIAhAYW/gXd8NJxTopVqsGEhtf+L9/ERU2k/HSMFQpKcYJeNFI9Be0LCMij8cYyjj5EHIivGBkwLfrbX3IF8PqumVmnDpEG+eDsKibPG2JwAAAABJRU5ErkJggg=='); + background-position: center; + background-repeat: no-repeat; + height: 14px; + position: absolute; + right: 7px; + top: 7px; + width: 14px; +} +.chrome-bootstrap .overlay .page .close-button:hover { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAnUlEQVR4XoWQQQ6CQAxFewjkJkMCyXgJPMk7AiYczyBeZEAX6AKctGIaN+bt+trk9wtGQc/IkhnoKGxqqiWxOSZalapWFZ6VrIUDExsN0a5JRBq9LoVOR0eEQMoEhKizXhhsn0p1sCWVo7CwOf1RytPL8CPvwuBUoHL6ugeK30CVD1TqK7V/hdpe+VNChhOzV8xWny/+xosHF8578W/Hmc1OOC3wmwAAAABJRU5ErkJggg=='); +} +.chrome-bootstrap .overlay .page .action-area { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: end; + display: -webkit-box; + padding: 14px 17px; +} +.chrome-bootstrap .overlay .page .action-area-right { + display: -webkit-box; +} +.chrome-bootstrap .overlay .page .button-strip { + -webkit-box-orient: horizontal; + display: -webkit-box; +} +.chrome-bootstrap .overlay .page .button-strip button { + -webkit-margin-start: 10px; + display: block; +} diff --git a/chrome-bootstrap/chrome-bootstrap.less b/chrome-bootstrap/chrome-bootstrap.less new file mode 100644 index 0000000..83d5b49 --- /dev/null +++ b/chrome-bootstrap/chrome-bootstrap.less @@ -0,0 +1,783 @@ +.chrome-bootstrap { + font-family: 'Segoe UI', 'Chrome Droid Sans', 'Droid Sans Fallback', 'Lucida Grande', 'Tahoma', sans-serif; + font-size: 12px; + color: #303942; + cursor: default; + margin: 0; + + a { + border: none; + color: #15C; + cursor: pointer; + text-decoration: underline; + font-weight: normal; + + &:hover, &:focus { + outline: none; + } + } + ul, ol { + padding: 0; + } + li { + list-style-type:none; + } + dl, dt, dd { + margin:0; + } + button { + cursor: pointer; + } + + + /* Headings + ============================================== */ + h1, h2, h3, h4 { + -webkit-user-select: none; + font-weight: normal; + line-height: 1; + + small { + font-size:15px; + margin:0 10px; + color: #53637D; + } + } + h1 { + -webkit-margin-after: 1em; + -webkit-margin-before: 21px; + -webkit-margin-start: 23px; + height: 18px; + font-size: 18px; + + a { + color: #5C6166; + text-decoration: none; + } + } + h3 { + color: black; + font-size: 1.2em; + margin-bottom: 0.8em; + } + h4 { + font-size: 1em; + margin-bottom: 5px; + } + + + /* Layout + ============================================== */ + .frame { + .navigation { + height: 100%; + -webkit-margin-start: 0; + position: fixed; + -webkit-margin-end: 15px; + width: 155px; + z-index: 3; + } + .view, .content { + width: 738px; + overflow-x: hidden; + } + .content { + padding-top: 55px; + p { + text-align: justify; + } + } + .with_controls .content { + padding-top: 104px; + } + .view { + -webkit-margin-start: 155px; + + a { + font: inherit; + } + } + .mainview > * { + -webkit-margin-start: -20px; + -webkit-transition: margin 100ms, opacity 100ms; + opacity: 0; + z-index: 0; + position: absolute; + top: 0; + display: block; + } + .mainview > .selected { + -webkit-margin-start: 0; + -webkit-transition: margin 200ms, opacity 200ms; + -webkit-transition-delay: 100ms; + z-index: 1; + opacity: 1; + } + } + + + /* Header + ============================================== */ + header { + position: fixed; + background-image: -webkit-linear-gradient(#FFF, #FFF 40%, rgba(255, 255, 255, 0.92)); + width: 738px; + z-index: 2; + + h1 { + padding: 21px 0 13px; + margin: 0; + border-bottom: 1px solid #EEE; + } + + .corner { + position: absolute; + right: 0px; + top: 21px; + + input[type="text"] { + width: 210px; + } + &.cancelable { + .delete { + opacity: 1; + top: 4px; + right: 5px; + } + } + + } + } + + + /* View sections + ============================================== */ + section { + -webkit-padding-start: 18px; + margin-bottom: 24px; + margin-top: 8px; + max-width: 600px; + + h3 { + -webkit-margin-start: -18px; + } + .row { + display: block; + margin: 0.65em 0; + } + } + + + /* Control bar + ============================================== */ + .controls { + -webkit-padding-end: 3px; + -webkit-padding-start: 4px; + -webkit-transition: padding 100ms, height 100ms, opacity 100ms; + border-bottom: 1px solid #EEE; + display: -webkit-box; + overflow: hidden; + padding: 13px 0; + position: relative; + + .text { + display: inline-block; + margin-top: 4px; + } + + .spacer { + -webkit-box-flex: 1; + } + } + + /* Pagination + ============================================== */ + ol.pagination { + li { + margin: 0 2px; + display: inline-block; + line-height: 25px; + } + a { + width: 25px; + height: 24px; + text-align: center; + display: block; + background: #F0F6FE; + text-decoration: none; + + &:hover, &.selected { + background: #8AAAED; + color: #FFF; + } + } + } + + /* Alert + ============================================== */ + .alert { + border-radius: 3px; + background: rgba(147, 184, 252, 0.2); + display: block; + position: relative; + padding: 10px 30px 10px 10px; + line-height: 17px; + } + .alert .delete { + top: 5px; + right: 6px; + opacity: 1; + } + + /* Tags + ============================================== */ + ul.tags { + li { + background: #8AAAED; + color: #FFF; + border-radius: 3px; + position: relative; + display: inline-block; + padding: 2px 5px; + + a { + color: #FFF; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + .delete { + opacity: 1; + position: relative; + display: inline-block; + width: 13px; + height: 12px; + top: 1px; + background-position-y: -1px; + } + } + } + + + /* Main menu + ============================================== */ + ul.menu { + -webkit-margin-before: 1em; + -webkit-margin-after: 2em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + -webkit-padding-start: 40px; + + list-style-type: none; + padding: 0; + + li { + -webkit-border-start: 6px solid transparent; + -webkit-padding-start: 18px; + -webkit-user-select: none; + display: list-item; + text-align: -webkit-match-parent; + + &.selected { + -webkit-border-start-color: rgb(78, 87, 100); + + a { + color: #464E5A; + } + } + + a { + border: 0; + color: #999; + cursor: pointer; + font: inherit; + line-height: 29px; + margin: 0; + padding: 0; + text-decoration: none; + display: block; + } + } + } + + + /* Icons + ============================================== */ + .arrow_collapse { + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 6px solid #999; + -webkit-margin-end: 4px; + top: 1px; + } + .arrow_expand { + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 7px solid #999; + -webkit-margin-end: 4px; + } + .arrow { + width: 0; + height: 0; + position: relative; + display: inline-block; + } + .delete { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg=="); + background-repeat: no-repeat; + display: block; + opacity: 0; + height: 14px; + width: 14px; + -webkit-transition: 150ms opacity; + background-color: transparent; + text-indent: -5000px; + position: absolute; + + &:hover { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII="); + } + } + + + /* Highlightable list + ============================================== */ + .highlightable { + li { + position: relative; + padding: 2px 0; + + &:hover > a:not(.action), + a:not(.action):focus { + background-color: #F0F6FE; + color: #555; + } + &:hover > .action { + opacity: 0.7; + } + a { + padding: 5px; + display: block; + position: relative; + z-index: 0; + text-decoration: none; + } + dt { + font-size: 105%; + margin-bottom: 3px; + } + dd { + color: #999; + overflow: hidden; + white-space: nowrap; + font-size: 10px; + margin-top: 5px; + } + .tags { + float: left; + margin-top: -1px; + font-size: 12px; + + li { + &:last-child { + margin-right: 5px; + } + &:hover > a:not(.action) { + background: #8AAAED; + color: #FFF; + } + a { + padding: 0; + } + } + + } + .action { + -webkit-appearance: none; + -webkit-transition: opacity 150ms; + background: #8AAAED; + border: none; + border-radius: 2px; + color: white; + opacity: 0; + margin-top: 0; + font-size: 10px; + padding: 1px 6px; + position: absolute; + top: 8px; + right: 32px; + -webkit-transition: 150ms opacity; + cursor: pointer; + + &:hover { + opacity: 1; + } + } + .highlightable { + -webkit-margin-start: 30px; + } + } + + &.editable { + .delete { + position: absolute; + top: 7px; + right: 5px; + } + li:hover > .delete { + opacity: 1; + } + } + + &.draggable { + .handle { + width: 8px; + height: 41px; + background-image: linear-gradient(to bottom, #c1c1c1 50%, rgba(255, 255, 255, 0) 0%); + background-position: center; + background-size: 100% 17%; + background-repeat: repeat-y; + visibility: hidden; + position: absolute; + top: 4px; + left: 2px; + + &:hover { + cursor: move; + cursor: -webkit-grab; + display: block; + } + &:after { + margin-left: 3px; + width: 2px; + height: 41px; + background: #F0F6FE; + content: ""; + display: block; + } + } + li:hover .handle { + visibility: visible; + z-index: 1; + } + li > .item { + padding-left: 20px; + } + } + } + .match { + background: #f2f37b; + display: inline-block; + margin: 0 1px; + } + + /* Input styling + ============================================== */ + select, + input[type='checkbox'], + input[type='radio'], + input[type='button'], + button { + -webkit-appearance: none; + -webkit-user-select: none; + background-image: -webkit-linear-gradient(#EDEDED, #EDEDED 38%, #DEDEDE); + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 2px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #444; + font: inherit; + margin: 0 1px 0 0; + text-shadow: 0 1px 0 #F0F0F0; + } + button.small { + padding: 1px 5px 2px; + min-height: 1em; + } + + input[type='checkbox']:checked::before { + -webkit-user-select: none; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wDBhYcG79aGIsAAACbSURBVBjTjdFBCkFhFAXgj4fp24PBy0SZ2ICRXRgYGb2xlKzBSEo2YgsiKWVoZgFKMjD5X/2Ux6lb99bpnNO5lKMR5i8MsEQHkhJiEzlS9HCqfiFWMUIt3AfsC3KKLCL30Qr7HfM4Ro4h6rhiEqmusIMKuphGqo+ogSPGcbYLzh91vdkXSHDDBk+0gxussS3rNcMCs+D6E18/9gLPPhbDshfzLgAAAABJRU5ErkJggg=="); + background-size: 100% 100%; + content: ''; + display: block; + height: 100%; + width: 100%; + } + html[dir='rtl'] input[type='checkbox']:checked::before { + -webkit-transform: scaleX(-1); + } + input[type='radio']:checked::before { + background-color: #666; + border-radius: 100%; + bottom: 3px; + content: ''; + display: block; + left: 3px; + position: absolute; + right: 3px; + top: 3px; + } + select { + -webkit-appearance: none; + -webkit-padding-end: 20px; + -webkit-padding-start: 6px; + /* OVERRIDE */ + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC), -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); + background-position: right center; + background-repeat: no-repeat; + } + select { + min-height: 2em; + min-width: 4em; + } + + html[dir='rtl'] select { + background-position: center left; + } + + input[type='checkbox'] { + bottom: 2px; + height: 13px; + position: relative; + vertical-align: middle; + width: 13px; + } + + input[type='radio'] { + /* OVERRIDE */ + border-radius: 100%; + bottom: 3px; + height: 15px; + position: relative; + vertical-align: middle; + width: 15px; + } + button { + -webkit-padding-end: 10px; + -webkit-padding-start: 10px; + min-height: 2em; + min-width: 4em; + } + input[type='text'], + input[type='number'], + input[type='search'] { + border: 1px solid #BFBFBF; + border-radius: 2px; + box-sizing: border-box; + color: #444; + font: inherit; + margin: 0; + min-height: 2em; + padding: 3px; + padding-bottom: 4px; + } + .radio, .checkbox { + margin: 0.65em 0; + } + + /* Focused --------------------------------- */ + + select, + input[type='checkbox'], + input[type='password'], + input[type='radio'], + input[type='search'], + input[type='text'], + input[type='number'], + button { + &:focus { + /* OVERRIDE */ + -webkit-transition: border-color 200ms; + /* We use border color because it follows the border radius (unlike outline). + * This is particularly noticeable on mac. */ + border-color: rgb(77, 144, 254); + outline: none; + } + } + + /* Disabled --------------------------------- */ + + button, + select { + &:disabled { + background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); + border-color: rgba(80, 80, 80, 0.2); + box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), + inset 0 1px 2px rgba(255, 255, 255, 0.75); + color: #aaa; + cursor: default; + } + } + select:disabled { + /* OVERRIDE */ + background-image: -webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAAXNSR0IArs4c6QAAAAd0SU1FB9sLAxYEBKriBmwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAABLSURBVCiRY2CgA4gC4jQ8OIpokxKBoKGh4T8uDJIn2rD///8rLFiwYCE2g0DiIHkSfIndQLIMwmYgRQYhG/j27dsmig1CMpCVGHUAo8FcsHfxfXQAAAAASUVORK5CYII=") 1x), -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); + } + input[type='checkbox'], + input[type='radio'] { + &:disabled { + opacity: .75; + } + } + input[type='search'], + input[type='number'], + input[type='text'] { + &:disabled { + color: #999; + } + } + + /* Hovering --------------------------------- */ + + select, + input[type='checkbox'], + input[type='radio'], + button { + &:hover:enabled { + background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); + border-color: rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), + inset 0 1px 2px rgba(255, 255, 255, 0.95); + color: black; + } + } + select:hover:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); + } + + /* Active --------------------------------- */ + + select, + input[type='checkbox'], + input[type='radio'], + button { + &:active:enabled { + background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); + box-shadow: none; + text-shadow: none; + } + } + select:active:enabled { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"), -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); + } + + + /* Modal + ============================================== */ + .overlay { + -webkit-box-align: center; + -webkit-box-orient: vertical; + -webkit-box-pack: center; + -webkit-transition: opacity .2s; + background-color: rgba(255, 255, 255, 0.75); + bottom: 0; + display: -webkit-box; + left: 0; + overflow: auto; + padding: 20px; + position: fixed; + right: 0; + top: 0; + z-index: 5; + opacity: 1; + + &.transparent { + opacity: 0; + + .page { + -webkit-transform: scale(0.99) translateY(-20px); + } + } + + .page { + -webkit-border-radius: 3px; + -webkit-box-orient: vertical; + -webkit-transition: 200ms -webkit-transform; + background: white; + box-shadow: 0 4px 23px 5px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0, 0, 0, 0.15); + color: #333; + display: -webkit-box; + min-width: 400px; + padding: 0; + position: relative; + overflow: hidden; + + @-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(1); + } + 40% { + -webkit-transform: scale(1.02); + } + 60% { + -webkit-transform: scale(1.02); + } + 100% { + -webkit-transform: scale(1); + } + } + + &.pulse { + -webkit-animation-duration: 180ms; + -webkit-animation-iteration-count: 1; + -webkit-animation-name: pulse; + -webkit-animation-timing-function: ease-in-out; + } + + h1 { + -webkit-padding-end: 24px; + -webkit-user-select: none; + color: #333; + font-size: 120%; + margin: 0; + padding: 14px 17px 14px; + text-shadow: white 0 1px 2px; + } + ul li { + padding: 5px 0; + } + ul.tags li { + padding: 2px 5px; + } + .content-area { + -webkit-box-flex: 1; + overflow: auto; + padding: 6px 17px 6px; + } + .close-button { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAUklEQVR4XqXPYQrAIAhAYW/gXd8NJxTopVqsGEhtf+L9/ERU2k/HSMFQpKcYJeNFI9Be0LCMij8cYyjj5EHIivGBkwLfrbX3IF8PqumVmnDpEG+eDsKibPG2JwAAAABJRU5ErkJggg=='); + background-position: center; + background-repeat: no-repeat; + height: 14px; + position: absolute; + right: 7px; + top: 7px; + width: 14px; + + &:hover { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAnUlEQVR4XoWQQQ6CQAxFewjkJkMCyXgJPMk7AiYczyBeZEAX6AKctGIaN+bt+trk9wtGQc/IkhnoKGxqqiWxOSZalapWFZ6VrIUDExsN0a5JRBq9LoVOR0eEQMoEhKizXhhsn0p1sCWVo7CwOf1RytPL8CPvwuBUoHL6ugeK30CVD1TqK7V/hdpe+VNChhOzV8xWny/+xosHF8578W/Hmc1OOC3wmwAAAABJRU5ErkJggg=='); + } + } + .action-area { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: end; + display: -webkit-box; + padding: 14px 17px; + } + .action-area-right { + display: -webkit-box; + } + .button-strip { + -webkit-box-orient: horizontal; + display: -webkit-box; + + button { + -webkit-margin-start: 10px; + display: block; + } + } + } + } +} diff --git a/chrome-bootstrap/chrome-bootstrap.min.css b/chrome-bootstrap/chrome-bootstrap.min.css new file mode 100644 index 0000000..52bd145 --- /dev/null +++ b/chrome-bootstrap/chrome-bootstrap.min.css @@ -0,0 +1 @@ +.chrome-bootstrap{font-family:'Segoe UI','Chrome Droid Sans','Droid Sans Fallback','Lucida Grande','Tahoma',sans-serif;font-size:12px;color:#303942;cursor:default;margin:0}.chrome-bootstrap a{border:none;color:#15c;cursor:pointer;text-decoration:underline;font-weight:normal}.chrome-bootstrap a:hover,.chrome-bootstrap a:focus{outline:none}.chrome-bootstrap ul,.chrome-bootstrap ol{padding:0}.chrome-bootstrap li{list-style-type:none}.chrome-bootstrap dl,.chrome-bootstrap dt,.chrome-bootstrap dd{margin:0}.chrome-bootstrap button{cursor:pointer}.chrome-bootstrap h1,.chrome-bootstrap h2,.chrome-bootstrap h3,.chrome-bootstrap h4{-webkit-user-select:none;font-weight:normal;line-height:1}.chrome-bootstrap h1 small,.chrome-bootstrap h2 small,.chrome-bootstrap h3 small,.chrome-bootstrap h4 small{font-size:15px;margin:0 10px;color:#53637d}.chrome-bootstrap h1{-webkit-margin-after:1em;-webkit-margin-before:21px;-webkit-margin-start:23px;height:18px;font-size:18px}.chrome-bootstrap h1 a{color:#5c6166;text-decoration:none}.chrome-bootstrap h3{color:#000;font-size:1.2em;margin-bottom:.8em}.chrome-bootstrap h4{font-size:1em;margin-bottom:5px}.chrome-bootstrap .frame .navigation{height:100%;-webkit-margin-start:0;position:fixed;-webkit-margin-end:15px;width:155px;z-index:3}.chrome-bootstrap .frame .view,.chrome-bootstrap .frame .content{width:738px;overflow-x:hidden}.chrome-bootstrap .frame .content{padding-top:55px}.chrome-bootstrap .frame .content p{text-align:justify}.chrome-bootstrap .frame .with_controls .content{padding-top:104px}.chrome-bootstrap .frame .view{-webkit-margin-start:155px}.chrome-bootstrap .frame .view a{font:inherit}.chrome-bootstrap .frame .mainview>*{-webkit-margin-start:-20px;-webkit-transition:margin 100ms,opacity 100ms;opacity:0;z-index:0;position:absolute;top:0;display:block}.chrome-bootstrap .frame .mainview>.selected{-webkit-margin-start:0;-webkit-transition:margin 200ms,opacity 200ms;-webkit-transition-delay:100ms;z-index:1;opacity:1}.chrome-bootstrap header{position:fixed;background-image:-webkit-linear-gradient(#fff,#fff 40%,rgba(255,255,255,.92));width:738px;z-index:2}.chrome-bootstrap header h1{padding:21px 0 13px;margin:0;border-bottom:1px solid #eee}.chrome-bootstrap header .corner{position:absolute;right:0;top:21px}.chrome-bootstrap header .corner input[type="text"]{width:210px}.chrome-bootstrap header .corner.cancelable .delete{opacity:1;top:4px;right:5px}.chrome-bootstrap section{-webkit-padding-start:18px;margin-bottom:24px;margin-top:8px;max-width:600px}.chrome-bootstrap section h3{-webkit-margin-start:-18px}.chrome-bootstrap section .row{display:block;margin:.65em 0}.chrome-bootstrap .controls{-webkit-padding-end:3px;-webkit-padding-start:4px;-webkit-transition:padding 100ms,height 100ms,opacity 100ms;border-bottom:1px solid #eee;display:-webkit-box;overflow:hidden;padding:13px 0;position:relative}.chrome-bootstrap .controls .text{display:inline-block;margin-top:4px}.chrome-bootstrap .controls .spacer{-webkit-box-flex:1}.chrome-bootstrap ol.pagination li{margin:0 2px;display:inline-block;line-height:25px}.chrome-bootstrap ol.pagination a{width:25px;height:24px;text-align:center;display:block;background:#f0f6fe;text-decoration:none}.chrome-bootstrap ol.pagination a:hover,.chrome-bootstrap ol.pagination a.selected{background:#8aaaed;color:#fff}.chrome-bootstrap .alert{border-radius:3px;background:rgba(147,184,252,.2);display:block;position:relative;padding:10px 30px 10px 10px;line-height:17px}.chrome-bootstrap .alert .delete{top:5px;right:6px;opacity:1}.chrome-bootstrap ul.tags li{background:#8aaaed;color:#fff;border-radius:3px;position:relative;display:inline-block;padding:2px 5px}.chrome-bootstrap ul.tags li a{color:#fff;text-decoration:none}.chrome-bootstrap ul.tags li a:hover{text-decoration:underline}.chrome-bootstrap ul.tags li .delete{opacity:1;position:relative;display:inline-block;width:13px;height:12px;top:1px;background-position-y:-1px}.chrome-bootstrap ul.menu{-webkit-margin-before:1em;-webkit-margin-after:2em;-webkit-margin-start:0;-webkit-margin-end:0;-webkit-padding-start:40px;list-style-type:none;padding:0}.chrome-bootstrap ul.menu li{-webkit-border-start:6px solid transparent;-webkit-padding-start:18px;-webkit-user-select:none;display:list-item;text-align:-webkit-match-parent}.chrome-bootstrap ul.menu li.selected{-webkit-border-start-color:#4e5764}.chrome-bootstrap ul.menu li.selected a{color:#464e5a}.chrome-bootstrap ul.menu li a{border:0;color:#999;cursor:pointer;font:inherit;line-height:29px;margin:0;padding:0;text-decoration:none;display:block}.chrome-bootstrap .arrow_collapse{border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:6px solid #999;-webkit-margin-end:4px;top:1px}.chrome-bootstrap .arrow_expand{border-left:5px solid transparent;border-right:5px solid transparent;border-top:7px solid #999;-webkit-margin-end:4px}.chrome-bootstrap .arrow{width:0;height:0;position:relative;display:inline-block}.chrome-bootstrap .delete{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==");background-repeat:no-repeat;display:block;opacity:0;height:14px;width:14px;-webkit-transition:150ms opacity;background-color:transparent;text-indent:-5000px;position:absolute}.chrome-bootstrap .delete:hover{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII=")}.chrome-bootstrap .highlightable li{position:relative;padding:2px 0}.chrome-bootstrap .highlightable li:hover>a:not(.action),.chrome-bootstrap .highlightable li a:not(.action):focus{background-color:#f0f6fe;color:#555}.chrome-bootstrap .highlightable li:hover>.action{opacity:.7}.chrome-bootstrap .highlightable li a{padding:5px;display:block;position:relative;z-index:0;text-decoration:none}.chrome-bootstrap .highlightable li dt{font-size:105%;margin-bottom:3px}.chrome-bootstrap .highlightable li dd{color:#999;overflow:hidden;white-space:nowrap;font-size:10px;margin-top:5px}.chrome-bootstrap .highlightable li .tags{float:left;margin-top:-1px;font-size:12px}.chrome-bootstrap .highlightable li .tags li:last-child{margin-right:5px}.chrome-bootstrap .highlightable li .tags li:hover>a:not(.action){background:#8aaaed;color:#fff}.chrome-bootstrap .highlightable li .tags li a{padding:0}.chrome-bootstrap .highlightable li .action{-webkit-appearance:none;-webkit-transition:opacity 150ms;background:#8aaaed;border:none;border-radius:2px;color:#fff;opacity:0;margin-top:0;font-size:10px;padding:1px 6px;position:absolute;top:8px;right:32px;-webkit-transition:150ms opacity;cursor:pointer}.chrome-bootstrap .highlightable li .action:hover{opacity:1}.chrome-bootstrap .highlightable li .highlightable{-webkit-margin-start:30px}.chrome-bootstrap .highlightable.editable .delete{position:absolute;top:7px;right:5px}.chrome-bootstrap .highlightable.editable li:hover>.delete{opacity:1}.chrome-bootstrap .highlightable.draggable .handle{width:8px;height:41px;background-image:linear-gradient(to bottom,#c1c1c1 50%,rgba(255,255,255,0) 0%);background-position:center;background-size:100% 17%;background-repeat:repeat-y;visibility:hidden;position:absolute;top:4px;left:2px}.chrome-bootstrap .highlightable.draggable .handle:hover{cursor:move;cursor:-webkit-grab;display:block}.chrome-bootstrap .highlightable.draggable .handle:after{margin-left:3px;width:2px;height:41px;background:#f0f6fe;content:"";display:block}.chrome-bootstrap .highlightable.draggable li:hover .handle{visibility:visible;z-index:1}.chrome-bootstrap .highlightable.draggable li>.item{padding-left:20px}.chrome-bootstrap .match{background:#f2f37b;display:inline-block;margin:0 1px}.chrome-bootstrap select,.chrome-bootstrap input[type='checkbox'],.chrome-bootstrap input[type='radio'],.chrome-bootstrap input[type='button'],.chrome-bootstrap button{-webkit-appearance:none;-webkit-user-select:none;background-image:-webkit-linear-gradient(#ededed,#ededed 38%,#dedede);border:1px solid rgba(0,0,0,.25);border-radius:2px;box-shadow:0 1px 0 rgba(0,0,0,.08),inset 0 1px 2px rgba(255,255,255,.75);color:#444;font:inherit;margin:0 1px 0 0;text-shadow:0 1px 0 #f0f0f0}.chrome-bootstrap button.small{padding:1px 5px 2px;min-height:1em}.chrome-bootstrap input[type='checkbox']:checked::before{-webkit-user-select:none;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wDBhYcG79aGIsAAACbSURBVBjTjdFBCkFhFAXgj4fp24PBy0SZ2ICRXRgYGb2xlKzBSEo2YgsiKWVoZgFKMjD5X/2Ux6lb99bpnNO5lKMR5i8MsEQHkhJiEzlS9HCqfiFWMUIt3AfsC3KKLCL30Qr7HfM4Ro4h6rhiEqmusIMKuphGqo+ogSPGcbYLzh91vdkXSHDDBk+0gxussS3rNcMCs+D6E18/9gLPPhbDshfzLgAAAABJRU5ErkJggg==");background-size:100% 100%;content:'';display:block;height:100%;width:100%}.chrome-bootstrap html[dir='rtl'] input[type='checkbox']:checked::before{-webkit-transform:scaleX(-1)}.chrome-bootstrap input[type='radio']:checked::before{background-color:#666;border-radius:100%;bottom:3px;content:'';display:block;left:3px;position:absolute;right:3px;top:3px}.chrome-bootstrap select{-webkit-appearance:none;-webkit-padding-end:20px;-webkit-padding-start:6px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC),-webkit-linear-gradient(#ededed,#ededed 38%,#dedede);background-position:right center;background-repeat:no-repeat}.chrome-bootstrap select{min-height:2em;min-width:4em}.chrome-bootstrap html[dir='rtl'] select{background-position:center left}.chrome-bootstrap input[type='checkbox']{bottom:2px;height:13px;position:relative;vertical-align:middle;width:13px}.chrome-bootstrap input[type='radio']{border-radius:100%;bottom:3px;height:15px;position:relative;vertical-align:middle;width:15px}.chrome-bootstrap button{-webkit-padding-end:10px;-webkit-padding-start:10px;min-height:2em;min-width:4em}.chrome-bootstrap input[type='text'],.chrome-bootstrap input[type='number'],.chrome-bootstrap input[type='search']{border:1px solid #bfbfbf;border-radius:2px;box-sizing:border-box;color:#444;font:inherit;margin:0;min-height:2em;padding:3px;padding-bottom:4px}.chrome-bootstrap .radio,.chrome-bootstrap .checkbox{margin:.65em 0}.chrome-bootstrap select:focus,.chrome-bootstrap input[type='checkbox']:focus,.chrome-bootstrap input[type='password']:focus,.chrome-bootstrap input[type='radio']:focus,.chrome-bootstrap input[type='search']:focus,.chrome-bootstrap input[type='text']:focus,.chrome-bootstrap input[type='number']:focus,.chrome-bootstrap button:focus{-webkit-transition:border-color 200ms;border-color:#4d90fe;outline:none}.chrome-bootstrap button:disabled,.chrome-bootstrap select:disabled{background-image:-webkit-linear-gradient(#f1f1f1,#f1f1f1 38%,#e6e6e6);border-color:rgba(80,80,80,.2);box-shadow:0 1px 0 rgba(80,80,80,.08),inset 0 1px 2px rgba(255,255,255,.75);color:#aaa;cursor:default}.chrome-bootstrap select:disabled{background-image:-webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAAXNSR0IArs4c6QAAAAd0SU1FB9sLAxYEBKriBmwAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAABLSURBVCiRY2CgA4gC4jQ8OIpokxKBoKGh4T8uDJIn2rD///8rLFiwYCE2g0DiIHkSfIndQLIMwmYgRQYhG/j27dsmig1CMpCVGHUAo8FcsHfxfXQAAAAASUVORK5CYII=") 1x),-webkit-linear-gradient(#f1f1f1,#f1f1f1 38%,#e6e6e6)}.chrome-bootstrap input[type='checkbox']:disabled,.chrome-bootstrap input[type='radio']:disabled{opacity:.75}.chrome-bootstrap input[type='search']:disabled,.chrome-bootstrap input[type='number']:disabled,.chrome-bootstrap input[type='text']:disabled{color:#999}.chrome-bootstrap select:hover:enabled,.chrome-bootstrap input[type='checkbox']:hover:enabled,.chrome-bootstrap input[type='radio']:hover:enabled,.chrome-bootstrap button:hover:enabled{background-image:-webkit-linear-gradient(#f0f0f0,#f0f0f0 38%,#e0e0e0);border-color:rgba(0,0,0,.3);box-shadow:0 1px 0 rgba(0,0,0,.12),inset 0 1px 2px rgba(255,255,255,.95);color:#000}.chrome-bootstrap select:hover:enabled{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"),-webkit-linear-gradient(#f0f0f0,#f0f0f0 38%,#e0e0e0)}.chrome-bootstrap select:active:enabled,.chrome-bootstrap input[type='checkbox']:active:enabled,.chrome-bootstrap input[type='radio']:active:enabled,.chrome-bootstrap button:active:enabled{background-image:-webkit-linear-gradient(#e7e7e7,#e7e7e7 38%,#d7d7d7);box-shadow:none;text-shadow:none}.chrome-bootstrap select:active:enabled{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQokWNgoAOIAuI0PDiKaJMSgYCZmfkbkPkfHYPEQfJEG/b//3+FBQsWLGRjY/uJbBCIDxIHyRNtGDYDyTYI3UA+Pr4vFBmEbODbt2+bKDYIyUBWYtQBAIRzRP/XKJ//AAAAAElFTkSuQmCC"),-webkit-linear-gradient(#e7e7e7,#e7e7e7 38%,#d7d7d7)}.chrome-bootstrap .overlay{-webkit-box-align:center;-webkit-box-orient:vertical;-webkit-box-pack:center;-webkit-transition:opacity .2s;background-color:rgba(255,255,255,.75);bottom:0;display:-webkit-box;left:0;overflow:auto;padding:20px;position:fixed;right:0;top:0;z-index:5;opacity:1}.chrome-bootstrap .overlay.transparent{opacity:0}.chrome-bootstrap .overlay.transparent .page{-webkit-transform:scale(.99) translateY(-20px)}.chrome-bootstrap .overlay .page{-webkit-border-radius:3px;-webkit-box-orient:vertical;-webkit-transition:200ms -webkit-transform;background:#fff;box-shadow:0 4px 23px 5px rgba(0,0,0,.2),0 2px 6px rgba(0,0,0,.15);color:#333;display:-webkit-box;min-width:400px;padding:0;position:relative;overflow:hidden}@-webkit-keyframes pulse{0%{-webkit-transform:scale(1)}40%{-webkit-transform:scale(1.02)}60%{-webkit-transform:scale(1.02)}100%{-webkit-transform:scale(1)}}.chrome-bootstrap .overlay .page.pulse{-webkit-animation-duration:180ms;-webkit-animation-iteration-count:1;-webkit-animation-name:pulse;-webkit-animation-timing-function:ease-in-out}.chrome-bootstrap .overlay .page h1{-webkit-padding-end:24px;-webkit-user-select:none;color:#333;font-size:120%;margin:0;padding:14px 17px 14px;text-shadow:white 0 1px 2px}.chrome-bootstrap .overlay .page ul li{padding:5px 0}.chrome-bootstrap .overlay .page ul.tags li{padding:2px 5px}.chrome-bootstrap .overlay .page .content-area{-webkit-box-flex:1;overflow:auto;padding:6px 17px 6px}.chrome-bootstrap .overlay .page .close-button{background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAUklEQVR4XqXPYQrAIAhAYW/gXd8NJxTopVqsGEhtf+L9/ERU2k/HSMFQpKcYJeNFI9Be0LCMij8cYyjj5EHIivGBkwLfrbX3IF8PqumVmnDpEG+eDsKibPG2JwAAAABJRU5ErkJggg==');background-position:center;background-repeat:no-repeat;height:14px;position:absolute;right:7px;top:7px;width:14px}.chrome-bootstrap .overlay .page .close-button:hover{background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAQAAAC1QeVaAAAAnUlEQVR4XoWQQQ6CQAxFewjkJkMCyXgJPMk7AiYczyBeZEAX6AKctGIaN+bt+trk9wtGQc/IkhnoKGxqqiWxOSZalapWFZ6VrIUDExsN0a5JRBq9LoVOR0eEQMoEhKizXhhsn0p1sCWVo7CwOf1RytPL8CPvwuBUoHL6ugeK30CVD1TqK7V/hdpe+VNChhOzV8xWny/+xosHF8578W/Hmc1OOC3wmwAAAABJRU5ErkJggg==')}.chrome-bootstrap .overlay .page .action-area{-webkit-box-align:center;-webkit-box-orient:horizontal;-webkit-box-pack:end;display:-webkit-box;padding:14px 17px}.chrome-bootstrap .overlay .page .action-area-right{display:-webkit-box}.chrome-bootstrap .overlay .page .button-strip{-webkit-box-orient:horizontal;display:-webkit-box}.chrome-bootstrap .overlay .page .button-strip button{-webkit-margin-start:10px;display:block} \ No newline at end of file diff --git a/chrome-bootstrap/package.json b/chrome-bootstrap/package.json new file mode 100644 index 0000000..87b8d7f --- /dev/null +++ b/chrome-bootstrap/package.json @@ -0,0 +1,16 @@ +{ + "name": "chrome-bootstrap", + "version": "1.4.0", + "description": "Reusable Chrome settings UI", + "author": "Roy Kolak ", + "repository": { + "type": "git", + "url": "http://github.com/roykolak/chrome-bootstrap.git" + }, + "scripts": { + "start": "./node_modules/less/bin/lessc chrome-bootstrap.less > chrome-bootstrap.css" + }, + "devDependencies": { + "less": "1.3.1" + } +} From 6a25101916b3b4c774504efcb92bc87fbe745731 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 25 Sep 2014 16:08:11 +0100 Subject: [PATCH 026/105] Add support for browser preferences --- .gitignore | 2 + Chrome/background.js | 29 +++++ Chrome/options.js | 51 ++++++++ Firefox/lib/main.js | 24 ++++ README.md | 21 ++- Safari.safariextension/background.html | 21 +++ bin/build.js | 174 ++++++++++++++++++++----- lib/BabelExt.js | 86 +++++++++++- lib/extension.js | 39 +++++- lib/settings.json | 61 +++++++++ sink.html | 41 ++++++ 11 files changed, 507 insertions(+), 42 deletions(-) create mode 100644 Chrome/options.js diff --git a/.gitignore b/.gitignore index a174dbf..ea5bffa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ /build *~ /Chrome/*.png +/Chrome/options.html /Firefox/*.png +/Safari.safariextension/Settings.plist diff --git a/Chrome/background.js b/Chrome/background.js index d7e9671..90a9b41 100644 --- a/Chrome/background.js +++ b/Chrome/background.js @@ -87,3 +87,32 @@ chrome.runtime.onMessage.addListener( } } ); + +// the simple "onMessage" interface only works when the response is sent sychronously. +// Because preferences need to respond after a delay, we have to use the full interface: +chrome.runtime.onConnect.addListener(function(port) { + console.assert(port.name == "delayedMessage"); + port.onMessage.addListener( + function(request) { + function sendResponse(response) { port.postMessage({ request: request, response: response }) } + // all requests expect a JSON object with requestType and then the relevant + // companion information... + switch(request.requestType) { + case 'preferences': + switch (request.operation) { + case 'getItem': + chrome.storage.local.get(request.itemName, function(items) { + sendResponse({status: true, key: request.itemName, value: items.hasOwnProperty(request.itemName) ? items[request.itemName] : default_preferences[request.itemName]}); + }); + break; + case 'setItem': + var toSet = {}; toSet[request.itemName] = request.itemValue; + chrome.storage.local.set(toSet, function() { + sendResponse({status: true, key: request.itemName, value: request.itemValue}); + }); + break; + } + } + } + ); +}); diff --git a/Chrome/options.js b/Chrome/options.js new file mode 100644 index 0000000..0d4e53a --- /dev/null +++ b/Chrome/options.js @@ -0,0 +1,51 @@ +// based on https://developer.chrome.com/extensions/options + +function get_preferences() { + var preferences = {}; + [].slice.call(document.querySelectorAll('.pref')).forEach(function(element) { + switch ( element.nodeName ) { + case 'INPUT': + switch ( element.type ) { + case 'checkbox': + if ( element.checked ) { + preferences[element.id] = + element.hasAttribute('data-on') ? parseInt(element.getAttribute('data-on'),10) : true; + } else { + preferences[element.id] = + element.hasAttribute('data-off') ? parseInt(element.getAttribute('data-off'),10) : false; + } + break; + case 'radio' : if ( element.checked ) preferences[element.name] = element.value; break; + case 'number': preferences[element.id] = parseInt(element.value,10); break; + case 'text' : preferences[element.id] = element.value; break; + } + break; + case 'SELECT': preferences[element.id] = element.value; break; + } + }); + return preferences; +} + +document.addEventListener('DOMContentLoaded', function() { + chrome.storage.local.get(get_preferences(), function(preferences) { + [].slice.call(document.querySelectorAll('.pref')).forEach(function(element) { + switch ( element.nodeName ) { + case 'INPUT': + switch ( element.type ) { + case 'checkbox': + element.checked = + preferences[element.id] == ( element.hasAttribute('data-on') ? element.getAttribute('data-on') : true ); + break; + case 'radio' : element.checked = preferences[element.name] == element.value; break; + case 'number': element.value = preferences[element.id]; break; + case 'text' : element.value = preferences[element.id]; break; + } + break; + case 'SELECT': element.value = preferences[element.id]; break; + } + }); + }); +}); + +document.addEventListener('click', function() { chrome.storage.local.set(get_preferences(), function() {}); }); +document.addEventListener('input', function() { chrome.storage.local.set(get_preferences(), function() {}); }); diff --git a/Firefox/lib/main.js b/Firefox/lib/main.js index 7f8c6a7..cc5ca51 100644 --- a/Firefox/lib/main.js +++ b/Firefox/lib/main.js @@ -10,6 +10,7 @@ var workers = []; var contextMenu = require("sdk/context-menu"); var priv = require("sdk/private-browsing"); var windows = require("sdk/windows").browserWindows; +var prefs = require("sdk/simple-prefs").prefs; // require chrome allows us to use XPCOM objects... const {Cc, Ci, Cu, Cr} = require("chrome"); @@ -144,6 +145,29 @@ pageMod.PageMod({ break; } break; + case 'preferences': + switch (request.operation) { + case 'getItem': + worker.postMessage({ + name: 'preferences', + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: prefs[request.itemName] + }); + break; + case 'setItem': + prefs[request.itemName] = request.itemValue; + worker.postMessage({ + name: 'preferences', + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: request.itemValue + }); + break; + } + break; case 'addURLToHistory': var isPrivate = priv.isPrivate(windows.activeWindow); if (isPrivate) { diff --git a/README.md b/README.md index 91e0862..897e051 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ has its own function calls and way of working, including, but not limited to: - Accessing and controlling tabs (i.e. opening a link in a new one and choosing if it's focused) - Cross domain http requests (extensions require) - Storing data (using HTML5 localStorage or similar/equivalent engines) +- Managing add-on preferences (which some browsers call options or settings) - Triggering notifications (desktop or browser, depending on the browser's particular level of support) - Adding URLs to history (to mark links as visited) - Note: this is a bit of a hack in all non-Chrome browsers... @@ -48,10 +49,11 @@ websites or functionality on the web. For this reason, functionality that is no by one or more of the 4 BabelExt browsers (Chrome, Firefox, Opera, Safari) may not be added to BabelExt. -BabelExt also isn't meant to handle building each browser's native settings consoles/panels, etc. -They're just too different from each other to try and abstract into a nice little package, -and with the 4 supported browsers all handling modern HTML/CSS/Javascript so well - it makes -sense (to me, anyhow) to build settings consoles and the like using those technologies. +Because each browser implements preferences in a slightly different way, BabelExt only supports +the baseline functionality that can be supported across all browsers. That might be enough if +you only need a few buttons and options, but with the 4 supported browsers all handling modern +HTML/CSS/Javascript so well - it makes sense (to me, anyhow) to build preference pages into the +site your extension is for. That's what I did with Reddit Enhancement Suite, and it has worked rather well. I am considering adding the automatic form rendering code from RES into BabelExt, but I will need to devote some @@ -128,6 +130,17 @@ recognized by Safari. Don't remove that from the name! - Further Safari development information can be found at [https://developer.apple.com/library/safari/#documentation/Tools/Conceptual/SafariExtensionGuide/Introduction/Introduction.html](https://developer.apple.com/library/safari/#documentation/Tools/Conceptual/SafariExtensionGuide/Introduction/Introduction.html) +## Resetting extension data ## + +If your extension uses storage or preferences, you will need to test the extension data with +different stored values. Apart from Safari, all the browsers let you creat add multiple +profiles ("users" in Chrome), so you might want to create throwaway profiles for use during +testing. + +Private browsing isn't much help here, as some private browsing data will be initialised from +your public data. If you find profiles too much effort, Chrome/Opera also let you clear +extension data by deleting all files matching /Local*/** + ## Releasing packages ## You need to release the first version of your extension by hand, because each site has slightly different requirements for their extensions. diff --git a/Safari.safariextension/background.html b/Safari.safariextension/background.html index b31ccf8..763285c 100644 --- a/Safari.safariextension/background.html +++ b/Safari.safariextension/background.html @@ -73,6 +73,27 @@ break; } break; + case 'preferences': + switch (request.operation) { + case 'getItem': + sendResponse({ + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: safari.extension.settings[request.itemName] + }, msgEvent); + break; + case 'setItem': + safari.extension.settings[request.itemName] = request.itemValue; + sendResponse({ + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: request.itemValue + }, msgEvent); + break; + } + break; case "contextMenus.create": contextMenuItems[request.obj.onclick] = request.obj; break; diff --git a/bin/build.js b/bin/build.js index 5be3c5b..89c13d1 100755 --- a/bin/build.js +++ b/bin/build.js @@ -329,6 +329,24 @@ settings.contentScriptFiles.unshift('BabelExt.js'); settings.pretty_domain = ( settings.mach_include_subdomain ? '*.' : '' ) + settings.match_domain; delete settings.environment_specific; +settings.preferences.forEach(function(preference) { + /* + * Known-but-unsupported types: + * color - not supported by Safari + * file - not supported by Safari + * directory - not supported by Safari + * control - not supported by Safari, not clear what we'd do with it anyway + */ + if ( preference.type.search(/^(bool|boolint|integer|string|menulist|radio)$/) == -1 ) { + console.log( + 'Preference type "' + preference.type + ' is not supported.\n' + + 'Please specify a valid preference type: bool, boolint, integer, string, menulist, radio\n' + ); + phantom.exit(1); + } +}); + + /* * Load settings from lib/local_settings.json */ @@ -429,6 +447,38 @@ function build_safari() { var xml_txt = '\n' + new XMLSerializer().serializeToString(document).replace(">",">\n") + "\n"; fs.write( 'Safari.safariextension/Info.plist', xml_txt ); + if ( settings.preferences ) + function build_dict( preference, values ) { + return '\t\n\t\tDefaultValue\n\t\t' + preference.value + '\n\t\tKey\n\t\t' + preference.name + '\n\t\tTitle\n\t\t' + preference.title + '' + + Object.keys(values).map(function(value) { + if ( typeof(values[value]) == 'string' ) return '\n\t\t' + value + '\n\t\t' + values[value] + ''; + else if ( typeof(values[value]) == 'number' ) return '\n\t\t' + value + '\n\t\t' + values[value] + ''; + else if ( typeof(values[value]) == 'boolean' ) return '\n\t\t' + value + '\n\t\t<' + values[value] + '/>'; + else /* must be an array */ return '\n\t\t' + value + '\n\t\t' + values[value].map(function(v) { return '\n\t\t\t'+v+''; }).join('') + '\n\t\t' + }).join('') + + '\n\t\n' + } + fs.write( + 'Safari.safariextension/Settings.plist', + '\n' + + '\n' + + '\n' + + '\n' + + settings.preferences.map(function(preference) { + switch ( preference.type ) { + case 'bool' : return build_dict( preference, { Type: 'CheckBox' } ); + case 'boolint' : return build_dict( preference, { Type: 'CheckBox', FalseValue: 0, TrueValue: 1 } ); + case 'integer' : return build_dict( preference, { Type: 'Slider' } ); + case 'string' : return build_dict( preference, { Type: 'TextField', Password: false } ); + case 'menulist': return build_dict( preference, { Type: 'ListBox', Titles: preference.options.map(function(o) { return o.label }), Values: preference.options.map(function(o) { return o.value }), } ); + case 'radio' : return build_dict( preference, { Type: 'RadioButtons', Titles: preference.options.map(function(o) { return o.label }), Values: preference.options.map(function(o) { return o.value }), } ); + } + }).join('') + + '\n' + + '\n', + 'w' + ); + } function build_firefox() { @@ -459,8 +509,9 @@ function build_firefox() { "id": settings.id, "name": settings.name }; - if (settings.icons[48]) { pkg.icon = settings.icons[48]; symbolicLink( '../lib/'+pkg.icon , 'Firefox/'+pkg.icon ); } - if (settings.icons[64]) { pkg.icon_64 = settings.icons[64]; symbolicLink( '../lib/'+pkg.icon_64, 'Firefox/'+pkg.icon_64 ); } + if (settings.icons[48] ) { pkg.icon = settings.icons[48]; symbolicLink( '../lib/'+pkg.icon , 'Firefox/'+pkg.icon ); } + if (settings.icons[64] ) { pkg.icon_64 = settings.icons[64]; symbolicLink( '../lib/'+pkg.icon_64, 'Firefox/'+pkg.icon_64 ); } + if (settings.preferences) { pkg.preferences = settings.preferences; } fs.write( 'Firefox/package.json', JSON.stringify(pkg, null, ' ' ) + "\n", 'w' ); // Copy scripts into place: @@ -544,36 +595,97 @@ function build_chrome() { 'late' : 'document_idle' }; + var manifest = { + "name": settings.title, + "author": settings.author, + "version": settings.version, + "manifest_version": 2, + "description": settings.description, + "background": { + "scripts": ["background.js"] + }, + "content_scripts": [ + { + "matches": [ "*://" + settings.pretty_domain + '/*' ], + "js": settings.contentScriptFiles, + "run_at": when_string[settings.contentScriptWhen] + } + ], + "icons": settings.icons, + "permissions": [ + "*://" + settings.pretty_domain + "/*", + "contextMenus", + "tabs", + "history", + "notifications" + ] + }; + + if ( settings.preferences ) { + manifest.options_page = "options.html"; + manifest.permissions.push('storage'); + manifest.background.scripts.unshift('preferences.js'); + + fs.write( + 'Chrome/' + manifest.background.scripts[0], + "var default_preferences = {" + + settings.preferences.map(function(preference) { + switch ( preference.type ) { + case 'bool' : return "'" + preference.name + "':" + (preference.value?'true':'false'); + case 'boolint': return "'" + preference.name + "':" + (preference.value?'1' :'0' ); + default : return "'" + preference.name + "':" + JSON.stringify(preference.value); + } + }).join(', ') + + "};\n", + 'w' + ); + + fs.write( + 'Chrome/' + manifest.options_page, + "\n" + + "\n" + + "" + settings.title + " Options\n" + + '\n' + + '\n' + + '
\n' + + '
\n' + + '

' + settings.title + '

\n' + + '
\n' + + settings.preferences.map(function(preference) { + switch ( preference.type ) { + case 'bool' : return '
\n'; + case 'boolint': return '
\n'; + case 'integer': return '
\n'; + case 'string': return '
\n'; + case 'menulist': + return '
' + preference.title + ':
' + ; + case 'radio': + return '

' + preference.title + '

' + + preference.options.map(function(option,index) { + return '
' + }).join('') + '
'; + } + }).join('') + + + '
\n' + + '
\n' + // no buttons because we apply changes on click, but the padding makes the page look better + '
\n' + + '
\n' + + "\n" + + "\n" + + "\n", + 'w' + ); + + } + // Create manifest.json: - fs.write( - 'Chrome/manifest.json', - JSON.stringify({ - "name": settings.title, - "author": settings.author, - "version": settings.version, - "manifest_version": 2, - "description": settings.description, - "background": { - "scripts": ["background.js"] - }, - "content_scripts": [ - { - "matches": [ "*://" + settings.pretty_domain + '/*' ], - "js": settings.contentScriptFiles, - "run_at": when_string[settings.contentScriptWhen] - } - ], - "icons": settings.icons, - "permissions": [ - "*://" + settings.pretty_domain + "/*", - "contextMenus", - "tabs", - "history", - "notifications" - ] - }, null, '\t' ) + "\n", - 'w' - ); + fs.write( 'Chrome/manifest.json', JSON.stringify(manifest, null, '\t' ) + "\n", 'w' ); // Copy scripts and icons into place: settings.contentScriptFiles.forEach(function(file) { hardLink( 'lib/'+file , 'Chrome/' + file ) }); diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 5ac6681..9559775 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -17,6 +17,7 @@ var BabelExt = (function(Global, unsafeGlobal) { var document = Global.document; var chrome = Global.chrome; var safari = Global.safari; + var delayedMessagePort; var entityMap = { "&": "&", "<": "<", @@ -42,6 +43,7 @@ var BabelExt = (function(Global, unsafeGlobal) { instance.callbackQueue.callbacks[msgEvent.callbackID](msgEvent.response); break; case 'localStorage': + case 'preferences': instance.callbackQueue.callbacks[msgEvent.callbackID](msgEvent); break; case 'createTab': @@ -73,6 +75,19 @@ var BabelExt = (function(Global, unsafeGlobal) { } } ); + delayedMessagePort = chrome.runtime.connect({name: "delayedMessage"}); + delayedMessagePort.onMessage.addListener(function(message) { + switch(message.request.requestType) { + case "preferences": + if (message.request.callbackID) { + callbackQueue.callbacks[message.request.callbackID](message.response); + } + break; + default: + // sendResponse({status: "unrecognized request type"}); + break; + } + }); } else if (typeof(safari) !== 'undefined') { // safari instance.detectedBrowser = 'Safari'; @@ -84,6 +99,7 @@ var BabelExt = (function(Global, unsafeGlobal) { instance.callbackQueue.callbacks[msgEvent.message.callbackID](msgEvent.message); break; case 'localStorage': + case 'preferences': instance.callbackQueue.callbacks[msgEvent.message.callbackID](msgEvent.message); break; case 'createTab': @@ -166,7 +182,14 @@ var BabelExt = (function(Global, unsafeGlobal) { } else { callback = function() {}; } - chrome.runtime.sendMessage(thisJSON, callback); + switch ( thisJSON.requestType ) { + case 'preferences': + delayedMessagePort.postMessage(thisJSON); + break; + default: + chrome.runtime.sendMessage(thisJSON, callback); + break; + } }; break; case 'Safari': @@ -312,6 +335,29 @@ var BabelExt = (function(Global, unsafeGlobal) { } }; + /* + * browserPreferences - abstracted functions for get/set/remove based on browser... + */ + instance.browserPreferences = { + get: function(key, callback) { + var thisJSON = { + requestType: 'preferences', + operation: 'getItem', + itemName: key + }; + instance.bgMessage(thisJSON, callback); + }, + set: function(key, value, callback) { + var thisJSON = { + requestType: 'preferences', + operation: 'setItem', + itemName: key, + itemValue: value + }; + instance.bgMessage(thisJSON, callback); + } + }; + /* * browserHistory - abstracted function for adding a URL to history * note: Only "add" is supported, because only Chrome supports any @@ -563,7 +609,7 @@ var BabelExt = (function(Global, unsafeGlobal) { function next_handler_storage() { function get(data) { - args.push(data.value); + args.push(data ? data.value : undefined); if ( handler.pass_storage.length ) BabelExt.storage.get(handler.pass_storage.shift(), get ); else @@ -581,7 +627,7 @@ var BabelExt = (function(Global, unsafeGlobal) { // retrieve arguments from preferences: function next_handler_preferences() { function get(data) { - args.push(data.value); + args.push(data ? data.value : undefined); if ( handler.pass_preferences.length ) BabelExt.preferences.get(handler.pass_preferences.shift(), get ); else @@ -597,11 +643,17 @@ var BabelExt = (function(Global, unsafeGlobal) { // execute handler: function next_handler_handle() { - if ( handler.callback.apply( document, args ) !== false ) - return next_handler(); + try { + if ( handler.callback.apply( document, args ) !== false ) + return next_handler(); + } catch (error) { + console.error( 'handler failed', handler, error.toString() ); + throw error; + }; } } + next_handler(); } @@ -654,7 +706,7 @@ var BabelExt = (function(Global, unsafeGlobal) { /* - * storage functions - handles opening and closing tabs, choosing if they're focused, etc. + * storage functions */ storage: { /* @@ -678,6 +730,28 @@ var BabelExt = (function(Global, unsafeGlobal) { remove: function(key, callback) { instance.browserStorage.remove(key, callback || function() {}); } + }, + + /* + * preference functions + */ + preferences: { + /* + * preferences.get - gets preferences for [key], calls callback with [return value] as parameter + */ + get: function(key, callback) { + if (typeof(callback) !== 'function') { + throw 'ERROR: no callback provided for BabelExt.preferences.get()'; + } + instance.browserPreferences.get(key, callback); + }, + /* + * preferences.set - sets preferences for [key] to [value], calls callback with key, value as parameters + */ + set: function(key, value, callback) { + instance.browserPreferences.set(key, value, callback || function() {}); + } + }, /* * history functions - handles adding a URL to browser history diff --git a/lib/extension.js b/lib/extension.js index cb86c63..19bdab3 100644 --- a/lib/extension.js +++ b/lib/extension.js @@ -55,7 +55,7 @@ // show each available kitchen sink demo, let the user know the extension is out of date if there's an unrecognized demo... var features = document.body.querySelectorAll('.featureContainer'); - var recognizedFeatures = ['save','load','tabCreate','notificationCreate','historyAdd', 'cssAdd']; + var recognizedFeatures = ['save','load','savePref','loadPref','tabCreate','notificationCreate','historyAdd', 'cssAdd']; for (var i=0, len=features.length; iAfter installing the extension, refresh this page to see the features that B +
+
+ + Save a preference... + +
+ +
+
+ + +
+ +
+
+
+
+ + Load a preference... + +
+ +
+
+ +
+
From f271b08a14aa1b8a9b488947bdc7a06cbc66e73a Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 26 Sep 2014 17:01:09 +0100 Subject: [PATCH 027/105] Correct a couple of line lengths in README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 897e051..e51ad03 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,11 @@ First, download all of the source from Github and put it together within a folde Then, download PhantomJS (http://phantomjs.org), which is used to build and deploy extensions. -In UNIX-based OSes, run `./bin/build.sh build` to build packages, and -`./bin/build.sh release` to release them. +In UNIX-based OSes, run `./bin/build.sh build` to build packages, and `./bin/build.sh release` +to release them. The build system hasn't been tested under Windows yet - your best bet is probably to look at -the shell script and write a Windows equivalent. If it's any good, please send in a patch! +the scripts and write a Windows equivalent. If it's any good, please send in a patch! **IMPORTANT SAFARI NOTE:** Safari has a "security feature" that is not documented, gives no user feedback at all, and can be a HUGE time sink if you don't know about it! If you have any From f0117c460fd93d1d6edd3ed0ff57067e08f71dce Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 26 Sep 2014 18:34:39 +0100 Subject: [PATCH 028/105] Support match_domain in Safari, support domain patterns everywhere The latter is supported by all browsers now old Opera is gone --- bin/build.js | 23 +++++++++++++++++++---- lib/settings.json | 6 +++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/bin/build.js b/bin/build.js index 89c13d1..9c328f5 100755 --- a/bin/build.js +++ b/bin/build.js @@ -326,7 +326,6 @@ if ( system.env.hasOwnProperty('ENVIRONMENT') ) { phantom.exit(1); }; settings.contentScriptFiles.unshift('BabelExt.js'); -settings.pretty_domain = ( settings.mach_include_subdomain ? '*.' : '' ) + settings.match_domain; delete settings.environment_specific; settings.preferences.forEach(function(preference) { @@ -419,6 +418,17 @@ function build_safari() { get_node('Description' ).textContent = settings.description; get_node('Website' ).textContent = settings.website; + var match_domains = get_node('Allowed Domains'); + while (match_domains.firstChild) match_domains.removeChild(match_domains.firstChild); + var domain = document.createElement("string"); + domain.textContent = settings.match_domain; + match_domains.appendChild( document.createTextNode('\n\t\t\t\t') ); + match_domains.appendChild(domain); + match_domains.appendChild( document.createTextNode('\n\t\t\t') ); + + var match_secure_domain = get_node('Include Secure Pages'); + match_secure_domain.parentNode.replaceChild(document.createElement((settings.match_secure_domain||false).toString()),match_secure_domain); + var start_scripts = get_node('Start'); var end_scripts = get_node('End'); @@ -492,7 +502,10 @@ function build_firefox() { // Create settings.js: fs.write( 'Firefox/lib/settings.js', - 'exports.include = ["http://' + settings.pretty_domain + '/*","https://' + settings.pretty_domain + '/*"];\n' + + ( settings.match_secure_domain + ? 'exports.include = ["http://' + settings.match_domain + '/*","https://' + settings.match_domain + '/*"];\n' + : 'exports.include = ["http://' + settings.match_domain + '/*"];\n' + ) + 'exports.contentScriptWhen = "' + when_string[settings.contentScriptWhen] + '";\n' + 'exports.contentScriptFile = ' + JSON.stringify(settings.contentScriptFiles) + ";\n" , @@ -595,6 +608,8 @@ function build_chrome() { 'late' : 'document_idle' }; + var match_url = ( settings.match_secure_domain ? "*://" : "http://" ) + settings.match_domain + '/*'; + var manifest = { "name": settings.title, "author": settings.author, @@ -606,14 +621,14 @@ function build_chrome() { }, "content_scripts": [ { - "matches": [ "*://" + settings.pretty_domain + '/*' ], + "matches": [ match_url ], "js": settings.contentScriptFiles, "run_at": when_string[settings.contentScriptWhen] } ], "icons": settings.icons, "permissions": [ - "*://" + settings.pretty_domain + "/*", + match_url, "contextMenus", "tabs", "history", diff --git a/lib/settings.json b/lib/settings.json index 9f59565..89608c6 100644 --- a/lib/settings.json +++ b/lib/settings.json @@ -40,9 +40,9 @@ "contentScriptWhen": "middle", "contentScriptFiles": [ "extension.js" ], - "match_domain": "babelext.com", - // whether to match *.: - "match_include_subdomains": false, + "match_domain": "babelext.com", // or *.babelext.com to include subdomains + // whether to match https://: + "match_secure_domain": true, "environment_specific": { /* From 82bbabcb9e13b85063a13444e100824112d3e6a7 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 26 Sep 2014 19:38:31 +0100 Subject: [PATCH 029/105] Fix typos in local_settings.json.example --- lib/local_settings.json.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/local_settings.json.example b/lib/local_settings.json.example index a471a47..23f2e08 100644 --- a/lib/local_settings.json.example +++ b/lib/local_settings.json.example @@ -15,7 +15,7 @@ // set this to release to the Chrome store: chrome_login_info: { - username: 'user@example.org' + username: 'user@example.org', password: 'password123', // optional - defaults to the environment variable 'CHROME_PASSWORD' // "ID" string in chrome://extensions/: @@ -42,7 +42,7 @@ username: 'user@example.org', password: 'password123', // optional - defaults to the environment variable 'OPERA_PASSWORD' tested_on: 'Opera 25 Developer Linux' // List of versions you've tested on (see opera://about for your version) - } + }, // Command to get the changelog for your latest version. // At the time of writing, this was required by Opera and hadn't yet been implemented for other browsers: From a4378971bafe863d290edf448c391bccde3337b2 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 26 Sep 2014 19:43:10 +0100 Subject: [PATCH 030/105] Changes to browser-specific files based on recent build changes --- Chrome/manifest.json | 5 +++-- Firefox/lib/settings.js | 2 +- Safari.safariextension/Info.plist | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Chrome/manifest.json b/Chrome/manifest.json index 4f4b522..d4d6f7e 100644 --- a/Chrome/manifest.json +++ b/Chrome/manifest.json @@ -23,10 +23,11 @@ ], "icons": {}, "permissions": [ - "*://babelext.com", + "*://babelext.com/*", "contextMenus", "tabs", "history", - "notifications" + "notifications", + "storage" ] } diff --git a/Firefox/lib/settings.js b/Firefox/lib/settings.js index 5d96ee2..c88ed14 100644 --- a/Firefox/lib/settings.js +++ b/Firefox/lib/settings.js @@ -1,3 +1,3 @@ exports.include = ["http://babelext.com/*","https://babelext.com/*"]; exports.contentScriptWhen = "ready"; -exports.contentScriptFile = ["self.data.url('BabelExt.js')","self.data.url('extension.js')"]; +exports.contentScriptFile = ["BabelExt.js","extension.js"]; diff --git a/Safari.safariextension/Info.plist b/Safari.safariextension/Info.plist index 38cc1c4..ed3327c 100644 --- a/Safari.safariextension/Info.plist +++ b/Safari.safariextension/Info.plist @@ -3,13 +3,13 @@ Author - Steve Sobel + honestbleeps Builder Version 8536.30.1 CFBundleDisplayName - BabelExt Extension + BabelExt CFBundleIdentifier - com.honestbleeps.babelext-extension + com.honestbleeps.abcdef01-2345-6789-9876-543210fedcba CFBundleInfoDictionaryVersion 6.0 CFBundleShortVersionString @@ -35,7 +35,7 @@ Command - + Identifier notificationButton Image From 0f5c6954a8c0c6927f5e5edd3fa2acab7e7f3416 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 26 Sep 2014 20:14:34 +0100 Subject: [PATCH 031/105] Better error-handling in bin/build.js --- bin/build.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/build.js b/bin/build.js index 9c328f5..c383432 100755 --- a/bin/build.js +++ b/bin/build.js @@ -544,10 +544,10 @@ function build_firefox() { // do it with `curl` instead: console.log( 'Unpacking Firefox Addon SDK...', status ); childProcess.execFile( 'curl', ['--silent',response.redirectURL,'-o','temporary_file.tar.gz'], null, function(err, stdout, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } fs.makeDirectory('firefox-addon-sdk'); childProcess.execFile( 'tar', ["zxf",'temporary_file.tar.gz','-C','firefox-addon-sdk','--strip-components=1'], null, function(err,stdout,stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } fs.remove('temporary_file.tar.gz'); fs.write( 'firefox-addon-sdk-url.txt', response.redirectURL, 'w' ); build_xpi(); @@ -571,7 +571,7 @@ function build_firefox() { // Move the .xpi into place, fix its install.rdf, and update firefox-unpacked: function finalise_xpi(err, stdout, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } fs.makeDirectory('build'); var xpi = 'build/' + settings.name + '.xpi'; if ( fs.exists(xpi) ) fs.remove(xpi); @@ -579,7 +579,7 @@ function build_firefox() { fs.removeTree('firefox-unpacked'); fs.makeDirectory('firefox-unpacked'); childProcess.execFile( 'unzip', ['-d','firefox-unpacked',xpi], null, function(err,stdout,stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } fs.write( 'firefox-unpacked/install.rdf', fs.read('firefox-unpacked/install.rdf').replace( /.*<\/em:maxVersion>/, '' + settings.firefox_max_version + '' ) @@ -591,7 +591,7 @@ function build_firefox() { fs.changeWorkingDirectory('firefox-unpacked'); childProcess.execFile( 'zip', ['../'+xpi,'install.rdf'], null, function(err,stdout,stderr) { fs.changeWorkingDirectory('..'); - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } console.log('Built ' + xpi + '\n\033[1mRemember to restart Firefox if you added/removed any files!\033[0m'); return program_counter.end(); }); @@ -713,7 +713,7 @@ function build_chrome() { build_crx(); } else { childProcess.execFile(chrome_command, ["--pack-extension=Chrome"], null, function (err, stdout, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } build_crx(); }); }; @@ -721,7 +721,7 @@ function build_chrome() { // Build the .crx, move it into place, and build the upload zip file: function build_crx() { childProcess.execFile(chrome_command, ["--pack-extension=Chrome","--pack-extension-key=Chrome.pem"], null, function (err, stdout, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } if ( stdout != 'Created the extension:\n\nChrome.crx\n' ) console.log(stdout.replace(/\n$/,'')); var crx = 'build/' + settings.name + '.crx'; if ( fs.exists(crx) ) fs.remove(crx); @@ -736,7 +736,7 @@ function build_chrome() { , null, function(err,stdout,stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } console.log('Built build/chrome-store-upload.zip'); return program_counter.end(); } @@ -1009,7 +1009,7 @@ function release_chrome(login_info) { // PhantomJS refuses to download chunked data, do it with `curl` instead: childProcess.execFile( 'curl', ["--silent","https://accounts.google.com/o/oauth2/token",'-d',post_data], null, function(err, json, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } upload_and_publish( JSON.parse(json) ); }); } From ff9e08d1236e838a271a3f88ecbbd8bfaf3b6d27 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 26 Sep 2014 21:48:43 +0100 Subject: [PATCH 032/105] Add stub support for building Safari packages --- bin/build.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bin/build.js b/bin/build.js index c383432..866a8f0 100755 --- a/bin/build.js +++ b/bin/build.js @@ -1119,6 +1119,18 @@ function release_opera(login_info) { } +function release_safari() { + /* + * Safari support is limited at the moment, as it's the least-used browser and the hardest to support. + * + * To release a Safari extension, start here: https://developer.apple.com/programs/safari/ + * For instructions on building a Safari extension package on the command line, start here: http://developer.streak.com/2013/01/how-to-build-safari-extension-using.html + * + * Patches welcome! + * + */ +} + /* * MAIN SECTION */ @@ -1151,6 +1163,7 @@ case 'release': case 'amo' : release_amo (local_settings. amo_login_info); break; case 'chrome': release_chrome(local_settings.chrome_login_info); break; case 'opera' : release_opera (local_settings. opera_login_info); break; + case 'safari': release_safari(local_settings.safari_login_info); break; } break; From 183264c42dd0ef598f7b29d4c72eb8d6da27649b Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 28 Sep 2014 22:25:06 +0100 Subject: [PATCH 033/105] Remove dangling symlinks when building Firefox packages --- .gitignore | 1 + Firefox/data/.gitignore | 1 - bin/build.js | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 Firefox/data/.gitignore diff --git a/.gitignore b/.gitignore index ea5bffa..bced4ff 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ /Chrome/*.png /Chrome/options.html /Firefox/*.png +/Firefox/data/*.js /Safari.safariextension/Settings.plist diff --git a/Firefox/data/.gitignore b/Firefox/data/.gitignore deleted file mode 100644 index dcaffc0..0000000 --- a/Firefox/data/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/*.js diff --git a/bin/build.js b/bin/build.js index 866a8f0..583cb48 100755 --- a/bin/build.js +++ b/bin/build.js @@ -528,6 +528,8 @@ function build_firefox() { fs.write( 'Firefox/package.json', JSON.stringify(pkg, null, ' ' ) + "\n", 'w' ); // Copy scripts into place: + fs.removeTree('Firefox/data'); // PhantomJS won't list dangling symlinks, so we have to just delete the directory and recreate it + fs.makeDirectory('Firefox/data'); settings.contentScriptFiles.forEach(function(file) { symbolicLink( '../../lib/'+file, 'Firefox/data/' + file ) }); program_counter.begin(); From 64ee20eea1bbd66bc1e6483b3d4d1749fb311962 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 28 Sep 2014 22:29:54 +0100 Subject: [PATCH 034/105] Move console.log support into BabelExt.js The development benefit is much higher than the cost --- lib/BabelExt.js | 26 ++++++++++++++++++++++++++ lib/console.js | 28 ---------------------------- lib/settings.json | 2 +- 3 files changed, 27 insertions(+), 29 deletions(-) delete mode 100644 lib/console.js diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 9559775..85f322e 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -5,6 +5,32 @@ var Global = typeof window !== 'undefined' ? window : this; var unsafeGlobal = typeof unsafeWindow !=='undefined' ? unsafeWindow : (Global||this); +/* + * You can't log to the normal console from within a content script. + * Console logging is extremely useful during development, + * so this script makes console logging work as expected. + */ +var console = (function() { + + function log_in_embedded_page(command, args) { + BabelExt.utils.runInEmbeddedPage('console.' + command + '.apply( console, ' + JSON.stringify(Array.prototype.slice.call( args, 0 )) + ')'); + } + + return { + + assert: function() { return log_in_embedded_page( 'assert', arguments ) }, + + log : function() { return log_in_embedded_page( 'log' , arguments ) }, + + trace : function() { return log_in_embedded_page( 'trace' , arguments ) }, + info : function() { return log_in_embedded_page( 'info' , arguments ) }, + warn : function() { return log_in_embedded_page( 'warn' , arguments ) }, + error : function() { return log_in_embedded_page( 'error' , arguments ) } + + }; + +})(); + var BabelExt = (function(Global, unsafeGlobal) { 'use strict'; // define private variables here... diff --git a/lib/console.js b/lib/console.js deleted file mode 100644 index bf78fe6..0000000 --- a/lib/console.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * You can't log to the normal console from within a content script. - * Console logging is extremely useful during development, - * so this script makes console logging work as expected. - * Remember to remove this script in production. - */ -window.console = (function() { - - function log_in_embedded_page(command, args) { - var script = document.createElement('script'); - script.textContent = 'console.' + command + '.apply( console, ' + JSON.stringify(Array.prototype.slice.call( args, 0 )) + ')'; - document.documentElement.appendChild(script); - } - - return { - - assert: function() { return log_in_embedded_page( 'assert', arguments ) }, - - log : function() { return log_in_embedded_page( 'log' , arguments ) }, - - trace : function() { return log_in_embedded_page( 'trace' , arguments ) }, - info : function() { return log_in_embedded_page( 'info' , arguments ) }, - warn : function() { return log_in_embedded_page( 'warn' , arguments ) }, - error : function() { return log_in_embedded_page( 'error' , arguments ) } - - }; - -})(); diff --git a/lib/settings.json b/lib/settings.json index 89608c6..2302e45 100644 --- a/lib/settings.json +++ b/lib/settings.json @@ -50,7 +50,7 @@ * variables from the relevant block will be used: */ "development": { - "contentScriptFiles": [ "console.js" ], + "contentScriptFiles": [], }, "test": { From 1c57c2bd057b311f702fdeb0161b8edc528d9446 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Wed, 15 Oct 2014 10:57:21 +0100 Subject: [PATCH 035/105] Always replace hardlinked files Switching branches breaks hardlinks, which we have no better way to detect --- bin/build.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/bin/build.js b/bin/build.js index 583cb48..5217bfe 100755 --- a/bin/build.js +++ b/bin/build.js @@ -59,16 +59,15 @@ function symbolicLink( source, target ) { * Create a hard link from source to target */ function hardLink( source, target ) { - if ( ! fs.exists(target) ) { - if ( system.os.name == 'windows' ) { - childProcess.execFile('mklink', ['/H',target,source], function(err, stdout, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); - }); - } else { - childProcess.execFile('ln', [source,target], null, function(err, stdout, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); - }); - } + if ( fs.exists(target) ) fs.remove(target); + if ( system.os.name == 'windows' ) { + childProcess.execFile('mklink', ['/H',target,source], function(err, stdout, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + }); + } else { + childProcess.execFile('ln', [source,target], null, function(err, stdout, stderr) { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + }); } } From 8b0c492f8edd8d4e7940dba39e70ee86cde8ecaa Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Wed, 15 Oct 2014 10:57:42 +0100 Subject: [PATCH 036/105] Validate version strings in bin/build.js --- bin/build.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bin/build.js b/bin/build.js index 5217bfe..4a51a55 100755 --- a/bin/build.js +++ b/bin/build.js @@ -327,6 +327,17 @@ if ( system.env.hasOwnProperty('ENVIRONMENT') ) { settings.contentScriptFiles.unshift('BabelExt.js'); delete settings.environment_specific; +if ( + settings.version.search(/^[0-9]+(?:\.[0-9]+){0,3}$/) || + settings.version.split('.').filter(function(number) { return number > 65535 }).length +) { + console.log( + 'Google Chrome will not accept version number "' + settings.version + '"\n' + + 'Please specify a version number containing 1-4 dot-separated integers between 0 and 65535' + ); + phantom.exit(1); +} + settings.preferences.forEach(function(preference) { /* * Known-but-unsupported types: From 8e9d8a95156556fa62c08fd4b16d97912d62c30b Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 19 Oct 2014 21:06:18 +0100 Subject: [PATCH 037/105] Fix line length in README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e51ad03..55e092c 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ extension data by deleting all files matching /Local*/* Date: Fri, 24 Oct 2014 14:47:19 +0100 Subject: [PATCH 038/105] Do not waste bandwidth loading images in bin/build.js --- bin/build.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/build.js b/bin/build.js index 4a51a55..7439fd1 100755 --- a/bin/build.js +++ b/bin/build.js @@ -264,6 +264,8 @@ function page( url, callback ) { page.showConsoleMessage(); + page.settings.loadImages = false; + return page.open( url, function(status) { if (status == 'success') { callback(page); From 833d20bf79b4b8b67f0ffb401cdb9c01cd48e6a7 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 24 Oct 2014 14:48:25 +0100 Subject: [PATCH 039/105] Remove unused variable in bin/build.js --- bin/build.js | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/build.js b/bin/build.js index 7439fd1..d2c0fb8 100755 --- a/bin/build.js +++ b/bin/build.js @@ -1018,7 +1018,6 @@ function release_chrome(login_info) { ); function get_auth_key(code) { - var page = webPage.create(); var post_data = "grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id=" + login_info.client_id + "&client_secret=" + login_info.client_secret + "&code=" + code; // PhantomJS refuses to download chunked data, do it with `curl` instead: From 7b89b49b35d7816c73597327f2f8602ec83fcfa8 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 24 Oct 2014 14:49:10 +0100 Subject: [PATCH 040/105] Handle problems downloading new versions of the Firefox SDK in bin/build.js --- bin/build.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/build.js b/bin/build.js index d2c0fb8..518c958 100755 --- a/bin/build.js +++ b/bin/build.js @@ -548,6 +548,11 @@ function build_firefox() { // Check whether the Addon SDK is up-to-date: var page = webPage.create(); + page.onResourceError = function(resourceError) { + console.log('Unable to load resource (' + resourceError.url + ')'); + console.log('Error code: ' + resourceError.errorCode + '. Description: ' + resourceError.errorString); + return program_counter.end(); + }; page.onResourceReceived = function(response) { if ( fs.exists('firefox-addon-sdk-url.txt') && fs.read('firefox-addon-sdk-url.txt') == response.redirectURL ) { console.log( 'Firefox Addon SDK is up-to-date.' ); From c27f2bd58303a3fe7bf38fe4bc55f5980294788f Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 24 Oct 2014 14:49:45 +0100 Subject: [PATCH 041/105] Allow any SSL protocol in bin/build.js AMO needs this now for some reason --- bin/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/build.js b/bin/build.js index 518c958..438aa7a 100755 --- a/bin/build.js +++ b/bin/build.js @@ -1,4 +1,4 @@ -#!/usr/bin/phantomjs +#!/usr/bin/phantomjs --ssl-protocol=any /* * Phantom JS build script From bd2866a22457a3b8620228d36044984cbcc65eb6 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 24 Oct 2014 20:13:46 +0100 Subject: [PATCH 042/105] Add Chrome option files in build.js --- bin/build.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/build.js b/bin/build.js index 438aa7a..2177983 100755 --- a/bin/build.js +++ b/bin/build.js @@ -655,10 +655,14 @@ function build_chrome() { ] }; + var extra_files = []; + if ( settings.preferences ) { manifest.options_page = "options.html"; manifest.permissions.push('storage'); manifest.background.scripts.unshift('preferences.js'); + extra_files.push('Chrome/'+manifest.background.scripts[0]); + extra_files.push('Chrome/'+manifest.options_page); fs.write( 'Chrome/' + manifest.background.scripts[0], @@ -750,6 +754,7 @@ function build_chrome() { childProcess.execFile( 'zip', ['build/chrome-store-upload.zip','Chrome/background.js','Chrome/manifest.json'] + .concat( extra_files ) .concat( settings.contentScriptFiles.map(function(file) { return 'Chrome/'+file }) ) .concat( Object.keys(settings.icons).map(function(key ) { return 'Chrome/' + settings.icons[key] }) ) , From 1df33b6ce8266115911d91551fcff3bd237387a5 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 27 Oct 2014 17:07:27 +0000 Subject: [PATCH 043/105] Delete unused Chrome files while building --- bin/build.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bin/build.js b/bin/build.js index 2177983..93dfce8 100755 --- a/bin/build.js +++ b/bin/build.js @@ -664,6 +664,12 @@ function build_chrome() { extra_files.push('Chrome/'+manifest.background.scripts[0]); extra_files.push('Chrome/'+manifest.options_page); + fs.list('Chrome').forEach(function(file) { + if ( file[0] == '.' ) return; + if ( file.search( /^(?:background\.js|chrome-bootstrap\.css|options\.js)$/ ) == 0 ) return; + fs.remove('Chrome/' + file); + }); + fs.write( 'Chrome/' + manifest.background.scripts[0], "var default_preferences = {" + From 2393537c8eddd01db4be0717df6e8305b657fd6f Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Wed, 29 Oct 2014 19:54:01 +0000 Subject: [PATCH 044/105] Work around failed attempts to get items from storage Someone using Chrome Portable had this problem - I suspect it was missing a save location or something --- Chrome/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Chrome/background.js b/Chrome/background.js index 90a9b41..95e9b7c 100644 --- a/Chrome/background.js +++ b/Chrome/background.js @@ -102,7 +102,7 @@ chrome.runtime.onConnect.addListener(function(port) { switch (request.operation) { case 'getItem': chrome.storage.local.get(request.itemName, function(items) { - sendResponse({status: true, key: request.itemName, value: items.hasOwnProperty(request.itemName) ? items[request.itemName] : default_preferences[request.itemName]}); + sendResponse({status: true, key: request.itemName, value: (items||{}).hasOwnProperty(request.itemName) ? items[request.itemName] : default_preferences[request.itemName]}); }); break; case 'setItem': From b61f62f76b6164fa6195783f9254ef947f13d162 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 7 Nov 2014 05:06:24 +0000 Subject: [PATCH 045/105] Another attempt at working around the Chrome storage issue --- Chrome/background.js | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/Chrome/background.js b/Chrome/background.js index 95e9b7c..27790c2 100644 --- a/Chrome/background.js +++ b/Chrome/background.js @@ -88,13 +88,22 @@ chrome.runtime.onMessage.addListener( } ); +var storage_local_works = false; +try { + chrome.storage.local.get('', function() {}); + storage_local_works = true; +} catch (e) { + console.log( 'chrome.storage.local disabled: ', e ); + console.log( 'This extension will still work, but will act as if all options have the default value.' ); +} + // the simple "onMessage" interface only works when the response is sent sychronously. // Because preferences need to respond after a delay, we have to use the full interface: chrome.runtime.onConnect.addListener(function(port) { console.assert(port.name == "delayedMessage"); - port.onMessage.addListener( - function(request) { - function sendResponse(response) { port.postMessage({ request: request, response: response }) } + if ( storage_local_works ) { + port.onMessage.addListener(function(request) { + function sendResponse(response) { port.postMessage({ request: request, response: response }) } // all requests expect a JSON object with requestType and then the relevant // companion information... switch(request.requestType) { @@ -113,6 +122,23 @@ chrome.runtime.onConnect.addListener(function(port) { break; } } - } - ); + }); + } else { + port.onMessage.addListener(function(request) { + function sendResponse(response) { port.postMessage({ request: request, response: response }) } + // all requests expect a JSON object with requestType and then the relevant + // companion information... + switch(request.requestType) { + case 'preferences': + switch (request.operation) { + case 'getItem': + sendResponse({status: true, key: request.itemName, value: default_preferences[request.itemName]}); + break; + case 'setItem': + sendResponse({status: false, key: request.itemName, value: request.itemValue}); + break; + } + } + }); + } }); From 5db0fafee0a77ea909b5a89d9274364474d5b839 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 7 Nov 2014 08:52:56 +0000 Subject: [PATCH 046/105] Exit with a non-zero value if any jobs failed in build.js --- bin/build.js | 57 ++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/bin/build.js b/bin/build.js index 93dfce8..dbde5ad 100755 --- a/bin/build.js +++ b/bin/build.js @@ -99,7 +99,7 @@ function _waitForEvent( test, callback ) { // low-level interface - see waitFor* console.log('Waited for ' + timeout + 'ms, but ' + failure_reason + ' - see fail.png and fail.html'); fs.write( 'fail.html', page.content ); page.render('fail.png'); - return program_counter.end(); + return program_counter.end(1); } return false; }; @@ -271,7 +271,7 @@ function page( url, callback ) { callback(page); } else { console.log( "Couln't connect to " + url ); - return program_counter.end(); + return program_counter.end(1); } }); } @@ -282,12 +282,13 @@ function page( url, callback ) { function AsyncCounter(zero_callback) { this.count = 0; + this.errors = 0; this.zero_callback = zero_callback } -AsyncCounter.prototype.begin = function() { ++this.count }; -AsyncCounter.prototype.end = function() { if ( !--this.count ) this.zero_callback() }; +AsyncCounter.prototype.begin = function( ) { ++this.count }; +AsyncCounter.prototype.end = function(errors) { this.errors += (errors||0); if ( !--this.count ) this.zero_callback(this.errors) }; -var program_counter = new AsyncCounter(function() { phantom.exit(0) }); +var program_counter = new AsyncCounter(function(errors) { phantom.exit(errors||0) }); /* * Load settings from lib/settings.json @@ -388,7 +389,7 @@ function get_changelog(callback) { // call the callback with the changelog text if (err) throw err; if ( changelog == '' ) { console.log( "Error: empty changelog" ); - return program_counter.end(); + return program_counter.end(1); } else { callback( local_settings.changelog = changelog ); } @@ -551,7 +552,7 @@ function build_firefox() { page.onResourceError = function(resourceError) { console.log('Unable to load resource (' + resourceError.url + ')'); console.log('Error code: ' + resourceError.errorCode + '. Description: ' + resourceError.errorString); - return program_counter.end(); + return program_counter.end(1); }; page.onResourceReceived = function(response) { if ( fs.exists('firefox-addon-sdk-url.txt') && fs.read('firefox-addon-sdk-url.txt') == response.redirectURL ) { @@ -563,10 +564,10 @@ function build_firefox() { // do it with `curl` instead: console.log( 'Unpacking Firefox Addon SDK...', status ); childProcess.execFile( 'curl', ['--silent',response.redirectURL,'-o','temporary_file.tar.gz'], null, function(err, stdout, stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } fs.makeDirectory('firefox-addon-sdk'); childProcess.execFile( 'tar', ["zxf",'temporary_file.tar.gz','-C','firefox-addon-sdk','--strip-components=1'], null, function(err,stdout,stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } fs.remove('temporary_file.tar.gz'); fs.write( 'firefox-addon-sdk-url.txt', response.redirectURL, 'w' ); build_xpi(); @@ -590,7 +591,7 @@ function build_firefox() { // Move the .xpi into place, fix its install.rdf, and update firefox-unpacked: function finalise_xpi(err, stdout, stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } fs.makeDirectory('build'); var xpi = 'build/' + settings.name + '.xpi'; if ( fs.exists(xpi) ) fs.remove(xpi); @@ -598,7 +599,7 @@ function build_firefox() { fs.removeTree('firefox-unpacked'); fs.makeDirectory('firefox-unpacked'); childProcess.execFile( 'unzip', ['-d','firefox-unpacked',xpi], null, function(err,stdout,stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } fs.write( 'firefox-unpacked/install.rdf', fs.read('firefox-unpacked/install.rdf').replace( /.*<\/em:maxVersion>/, '' + settings.firefox_max_version + '' ) @@ -610,9 +611,9 @@ function build_firefox() { fs.changeWorkingDirectory('firefox-unpacked'); childProcess.execFile( 'zip', ['../'+xpi,'install.rdf'], null, function(err,stdout,stderr) { fs.changeWorkingDirectory('..'); - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } console.log('Built ' + xpi + '\n\033[1mRemember to restart Firefox if you added/removed any files!\033[0m'); - return program_counter.end(); + return program_counter.end(0); }); }); } @@ -742,7 +743,7 @@ function build_chrome() { build_crx(); } else { childProcess.execFile(chrome_command, ["--pack-extension=Chrome"], null, function (err, stdout, stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } build_crx(); }); }; @@ -750,7 +751,7 @@ function build_chrome() { // Build the .crx, move it into place, and build the upload zip file: function build_crx() { childProcess.execFile(chrome_command, ["--pack-extension=Chrome","--pack-extension-key=Chrome.pem"], null, function (err, stdout, stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } if ( stdout != 'Created the extension:\n\nChrome.crx\n' ) console.log(stdout.replace(/\n$/,'')); var crx = 'build/' + settings.name + '.crx'; if ( fs.exists(crx) ) fs.remove(crx); @@ -766,9 +767,9 @@ function build_chrome() { , null, function(err,stdout,stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } console.log('Built build/chrome-store-upload.zip'); - return program_counter.end(); + return program_counter.end(0); } ); }); @@ -789,7 +790,7 @@ function release_amo(login_info) { login_info.password = system.env.AMO_PASSWORD; } else { console.log("Please specify a password for addons.mozilla.org"); - return program_counter.end(); + return program_counter.end(1); } } @@ -919,7 +920,7 @@ function release_amo(login_info) { '.notification-box.success', function() { console.log('Released to https://addons.mozilla.org/en-US/firefox/addon/' + name); - return program_counter.end(); + return program_counter.end(0); } ); } @@ -942,7 +943,7 @@ function release_chrome(login_info) { login_info.password = system.env.CHROME_PASSWORD; } else { console.log("Please specify a password for the Chrome store"); - return program_counter.end(); + return program_counter.end(1); } } @@ -1038,7 +1039,7 @@ function release_chrome(login_info) { // PhantomJS refuses to download chunked data, do it with `curl` instead: childProcess.execFile( 'curl', ["--silent","https://accounts.google.com/o/oauth2/token",'-d',post_data], null, function(err, json, stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(); } + if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } upload_and_publish( JSON.parse(json) ); }); } @@ -1060,7 +1061,7 @@ function release_chrome(login_info) { var result = JSON.parse(page.plainText); if ( result.error ) { console.log( page.plainText ); - return program_counter.end(); + return program_counter.end(1); } page.open( "https://www.googleapis.com/chromewebstore/v1.1/items/" + login_info.id + "/publish", @@ -1069,14 +1070,14 @@ function release_chrome(login_info) { function (status) { if ( result.error ) { console.log( page.plainText ); - return program_counter.end(); + return program_counter.end(1); } if ( status == "success" ) { console.log('Released to https://chrome.google.com/webstore/detail/' + login_info.id); - return program_counter.end(); + return program_counter.end(0); } else { console.log( "Couln't upload new version" ); - return program_counter.end(); + return program_counter.end(1); } } ); @@ -1098,7 +1099,7 @@ function release_opera(login_info) { login_info.password = system.env.OPERA_PASSWORD; } else { console.log("Please specify a password for the Opera Developer site"); - return program_counter.end(); + return program_counter.end(1); } } @@ -1135,7 +1136,7 @@ function release_opera(login_info) { [ '#dev-sel-container' ], function() { console.log('Released to https://addons.opera.com/en-gb/extensions/details/' + settings.name); - return program_counter.end(); + return program_counter.end(0); } ); } @@ -1200,4 +1201,4 @@ default: usage(); } -program_counter.end(); +program_counter.end(0); From 5af8e85d356ec224568c74037cdc752d64b92450 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Tue, 11 Nov 2014 07:58:54 +0000 Subject: [PATCH 047/105] Working workaround for the Chrome storage issue Some odd Chrome behaviour made me think the code was throwing an error. In fact it was just badly-worded debugging information. The real issue was chrome.storage.local.get() being super-slow (and perhaps timing out). This workaround uses the fallback implementation if it detects a timeout. --- Chrome/background.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Chrome/background.js b/Chrome/background.js index 27790c2..6c99a30 100644 --- a/Chrome/background.js +++ b/Chrome/background.js @@ -88,20 +88,28 @@ chrome.runtime.onMessage.addListener( } ); -var storage_local_works = false; +// chrome.storage.local should return almost instantly, but has been seen in the wild timing out. +// We do a test request first, and use a fallback implementation if that takes too long. +// The fallback implementation always returns default values. +var storage_local_works = true, storage_start_time = new Date().getTime(); try { - chrome.storage.local.get('', function() {}); - storage_local_works = true; + chrome.storage.local.get('', function() { + storage_local_works = new Date().getTime() - storage_start_time < 1000; + if (!storage_local_works) { + console.log( 'chrome.storage.local took too long to respond - disabing.', chrome.runtime.lastError ); + } + }); } catch (e) { - console.log( 'chrome.storage.local disabled: ', e ); - console.log( 'This extension will still work, but will act as if all options have the default value.' ); + storage_local_works = false; + console.log('chrome.storage.local disabled: ', e); + console.log('This extension will still work, but will act as if all options have the default value.'); } // the simple "onMessage" interface only works when the response is sent sychronously. // Because preferences need to respond after a delay, we have to use the full interface: chrome.runtime.onConnect.addListener(function(port) { console.assert(port.name == "delayedMessage"); - if ( storage_local_works ) { + if (storage_local_works) { // default behaviour port.onMessage.addListener(function(request) { function sendResponse(response) { port.postMessage({ request: request, response: response }) } // all requests expect a JSON object with requestType and then the relevant @@ -123,11 +131,9 @@ chrome.runtime.onConnect.addListener(function(port) { } } }); - } else { + } else { // fallback behaviour - return default values without waiting for the storage system port.onMessage.addListener(function(request) { function sendResponse(response) { port.postMessage({ request: request, response: response }) } - // all requests expect a JSON object with requestType and then the relevant - // companion information... switch(request.requestType) { case 'preferences': switch (request.operation) { From e7baf635a35b34c3b0114283dae5f93fca96181f Mon Sep 17 00:00:00 2001 From: Tony Lowry Date: Mon, 17 Nov 2014 17:47:54 +0000 Subject: [PATCH 048/105] Support for contextMenu.remove --- Chrome/background.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Chrome/background.js b/Chrome/background.js index d7e9671..3c806b1 100644 --- a/Chrome/background.js +++ b/Chrome/background.js @@ -81,6 +81,9 @@ chrome.runtime.onMessage.addListener( } chrome.contextMenus.create(request.obj); break; + case 'contextMenus.remove': + chrome.contextMenus.remove(request.obj.id); + break; default: sendResponse({status: "unrecognized request type"}); break; From 529ece6d3e32410b860c4839c5c6c717ff743886 Mon Sep 17 00:00:00 2001 From: Tony Lowry Date: Mon, 17 Nov 2014 17:48:56 +0000 Subject: [PATCH 049/105] namespace changes for new firefox SDK and support for contextMenu.remove. --- Firefox/lib/main.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/Firefox/lib/main.js b/Firefox/lib/main.js index 6d09acc..607e242 100644 --- a/Firefox/lib/main.js +++ b/Firefox/lib/main.js @@ -1,14 +1,14 @@ // Import the APIs we need. -var pageMod = require("page-mod"); -var Request = require("request").Request; -var notifications = require("notifications"); -var self = require("self"); -var tabs = require("tabs"); -var ss = require("simple-storage"); +var pageMod = require("sdk/page-mod"); +var Request = require("sdk/request").Request; +var notifications = require("sdk/notifications"); +var self = require("sdk/self"); +var tabs = require("sdk/tabs"); +var ss = require("sdk/simple-storage"); var workers = []; -var contextMenu = require("context-menu"); -var priv = require("private-browsing"); +var contextMenu = require("sdk/context-menu"); +var priv = require("sdk/private-browsing"); var windows = require("sdk/windows").browserWindows; // require chrome allows us to use XPCOM objects... @@ -173,6 +173,18 @@ pageMod.PageMod({ } }); + break; + case 'contextMenus.remove': + // Run through the current context items and destroy the one with a matching name + contextItems = contextMenu.contentContextMenu.items; + var len = contextItems.length; + for(var i =0; i < len; ++i){ + if(request.obj.title == contextItems[i].label){ + contextMenu.contentContextMenu.destroy(contextItems[i]); + break; + } + } + break; default: worker.postMessage({status: "unrecognized request type"}); From 0efdf690db3bd15465f6e3d46d2c3cce37cdc81b Mon Sep 17 00:00:00 2001 From: Tony Lowry Date: Mon, 17 Nov 2014 17:49:54 +0000 Subject: [PATCH 050/105] Support for contextMenu.remove --- lib/BabelExt.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index bce9116..09a4e3f 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -488,7 +488,66 @@ var BabelExt = (function(Global, unsafeGlobal) { break; } + }, + + /** + * Removes a context menu matching the given id + * + * @param {Object} obj An object that specifies type, id, title and "checked" - optional default boolean + * @param {Function} callback [description] + * @return {[type]} [description] + */ + remove: function(obj, callback) { + var thisJSON, callbackID; + + switch (instance.detectedBrowser) { + case 'Chrome': + // send a message to the background page to remove this context menu + + thisJSON = { + requestType: 'contextMenus.remove', + obj: obj, + callback: callback + }; + instance.bgMessage(thisJSON); // todo: add callback support + + break; + case 'Firefox': + // send a message to the background page to create this context menu + if (typeof obj.onclick === 'function') { + callbackID = callbackQueue.add(obj.onclick); + obj.onclick = callbackID; + } + + thisJSON = { + requestType: 'contextMenus.remove', + obj: obj, + callback: callback + }; + instance.bgMessage(thisJSON); // todo: add callback support + + break; + case 'Opera': + break; + case 'Safari': + // send a message to the background page to create this context menu + if (typeof obj.onclick === 'function') { + callbackID = callbackQueue.add(obj.onclick); + obj.onclick = callbackID; } + + thisJSON = { + requestType: 'contextMenus.remove', + obj: obj, + callback: callback + }; + instance.bgMessage(thisJSON); // todo: add callback support + + break; + } + + } + }; return { // public interface @@ -640,6 +699,9 @@ var BabelExt = (function(Global, unsafeGlobal) { contextMenu: { create: function(obj, callback) { instance.contextMenu.create(obj, callback); + }, + remove: function(obj, callback) { + instance.contextMenu.remove(obj, callback); } }, From fdd1616aa4b10093412c9c6e63e7b11a1e10a891 Mon Sep 17 00:00:00 2001 From: Tony Lowry Date: Mon, 17 Nov 2014 18:14:32 +0000 Subject: [PATCH 051/105] use the context menu title instead of id for chrome too. this fits better with existing examples and lets the user omit the id. --- Chrome/background.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Chrome/background.js b/Chrome/background.js index 3c806b1..5ab8643 100644 --- a/Chrome/background.js +++ b/Chrome/background.js @@ -79,10 +79,12 @@ chrome.runtime.onMessage.addListener( contextMenuClick(info, tab, callbackID); }; } + // id not available on firefox but title is, use it as common id + request.obj.id = request.obj.title; chrome.contextMenus.create(request.obj); break; case 'contextMenus.remove': - chrome.contextMenus.remove(request.obj.id); + chrome.contextMenus.remove(request.obj.title); break; default: sendResponse({status: "unrecognized request type"}); From 2c3dc886bdb8cb2ca2224903cb62df8350e5da31 Mon Sep 17 00:00:00 2001 From: Tony Lowry Date: Mon, 17 Nov 2014 18:16:30 +0000 Subject: [PATCH 052/105] use context menu remove function to clean up stale context menus when the user reloads the page. --- lib/extension.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/extension.js b/lib/extension.js index 0fffac9..979e39e 100644 --- a/lib/extension.js +++ b/lib/extension.js @@ -168,7 +168,12 @@ } }); } - + + // clean stale menu in case user reloads page + BabelExt.contextMenu.remove({ + title: 'Say hi', + }); + BabelExt.contextMenu.create({ type: 'normal', title: 'Say hi', From 41c87016ef6ac08fc42f526551553055aaaa4556 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 20 Nov 2014 14:46:38 +0000 Subject: [PATCH 053/105] Fix BabelExt.utils.dispatch()'s detection of missing parameters --- lib/BabelExt.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 85f322e..2911cf5 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -579,8 +579,6 @@ var BabelExt = (function(Global, unsafeGlobal) { return string == (""+tests); } else if ( tests instanceof RegExp ) { return tests.test(string); - } else if ( typeof(tests) == "boolean" ) { - return tests; } else { throw "Only arrays, strings, numbers, RegExps and booleans are allowed in match_*"; } @@ -597,7 +595,11 @@ var BabelExt = (function(Global, unsafeGlobal) { if ( handler.match_params ) { for ( var param in handler.match_params ) if ( handler.match_params.hasOwnProperty(param) ) - if ( !params.hasOwnProperty(param) || !check_match( params[param], handler.match_params[param] ) ) + if ( + typeof(handler.match_params[param]) == 'boolean' + ? params.hasOwnProperty(param) != handler.match_params[param] + : !( params.hasOwnProperty(param) && check_match( params[param], handler.match_params[param] ) ) + ) return next_handler(); } From 404bd22699767a4442f57209da16e96370d16d65 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sat, 14 Feb 2015 00:43:05 +0000 Subject: [PATCH 054/105] Fix whitespace issues --- lib/BabelExt.js | 22 +++++++++++----------- lib/extension.js | 10 +++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 6f08532..873f786 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -146,7 +146,7 @@ var BabelExt = (function(Global, unsafeGlobal) { /* * callbackQueue is a queue of callbacks to be executed on return data from the background page. - * This is necessary due to the double-layered asynchronous calls we're making. Calls to + * This is necessary due to the double-layered asynchronous calls we're making. Calls to * background pages are asynchronous as it is, so when we make an asynchronous call from the * foreground page to the background, we need to hold on to a reference to our callback that lives * in the context of the foreground, because Safari and Firefox do not allow you to pass @@ -161,7 +161,7 @@ var BabelExt = (function(Global, unsafeGlobal) { createId: function(prefix) { var rnd = Math.floor((Math.random()*65535)+1); if (prefix) rnd = prefix+rnd; - + if (this.callbacks.hasOwnProperty(rnd)) { return this.createId(prefix); } @@ -191,7 +191,7 @@ var BabelExt = (function(Global, unsafeGlobal) { * key: [key of item], * value: [value of item, if relevant] * } - */ + */ switch(instance.detectedBrowser) { case 'Firefox': instance.bgMessage = function(thisJSON, callback) { @@ -275,7 +275,7 @@ var BabelExt = (function(Global, unsafeGlobal) { }; /* - * tabs - abstracted functions for creating tabs, choosing if focused, and when applicable + * tabs - abstracted functions for creating tabs, choosing if focused, and when applicable * assigning them an index (not supported in Firefox or Safari) */ instance.browserTabs = { @@ -454,7 +454,7 @@ var BabelExt = (function(Global, unsafeGlobal) { /** * Creates a context menu with the provided parameters, and calls the * provided callback function upon click - * + * * @param {Object} obj An object that specifies type, id, title and "checked" - optional default boolean * @param {Function} callback [description] * @return {[type]} [description] @@ -511,10 +511,10 @@ var BabelExt = (function(Global, unsafeGlobal) { } }, - + /** * Removes a context menu matching the given id - * + * * @param {Object} obj An object that specifies type, id, title and "checked" - optional default boolean * @param {Function} callback [description] * @return {[type]} [description] @@ -525,7 +525,7 @@ var BabelExt = (function(Global, unsafeGlobal) { switch (instance.detectedBrowser) { case 'Chrome': // send a message to the background page to remove this context menu - + thisJSON = { requestType: 'contextMenus.remove', obj: obj, @@ -569,7 +569,7 @@ var BabelExt = (function(Global, unsafeGlobal) { } } - + }; instance.params = {}; @@ -585,11 +585,11 @@ var BabelExt = (function(Global, unsafeGlobal) { utils: { /* * merge: merges two objects (useful for creating defaults for functions that take a data object as a parameter) - * - + * - * */ merge: function(objA, objB) { - for (var key in objA) { + for (var key in objA) { if (key in objB) { continue; } objB[key] = objA[key]; } diff --git a/lib/extension.js b/lib/extension.js index e4662ae..67ce8ed 100644 --- a/lib/extension.js +++ b/lib/extension.js @@ -39,7 +39,7 @@ /* BEGIN KITCHEN SINK DEMO CODE */ /* - * The code below is for testing / execution on the BabelExt website at: + * The code below is for testing / execution on the BabelExt website at: * http://babelext.com/demo.html * * You should remove this code, and replace it with your own! @@ -73,7 +73,7 @@ if (keyEle && valueEle) { var key = keyEle.value; var val = valueEle.value; - BabelExt.storage.set(key, val, function() { + BabelExt.storage.set(key, val, function() { var keyEle = document.getElementById('setKey'); var valueEle = document.getElementById('setValue'); keyEle.value = ''; @@ -191,12 +191,12 @@ } }); } - + // clean stale menu in case user reloads page BabelExt.contextMenu.remove({ title: 'Say hi', }); - + BabelExt.contextMenu.create({ type: 'normal', title: 'Say hi', @@ -261,4 +261,4 @@ // So if an element doesn't exist on a page, it will block all later handlers in the same dispatch() /* END KITCHEN SINK DEMO CODE */ -})(); \ No newline at end of file +})(); From 78f4ce99f2c663c8793d840d3e10c0170606ba1d Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sat, 14 Feb 2015 01:09:09 +0000 Subject: [PATCH 055/105] Reorganised build files This should make the directory tree more readable for newbies --- .gitignore | 20 ++--- README.md | 6 +- bin/build.js | 84 +++++++++--------- {Chrome => build/Chrome}/.gitignore | 0 {Chrome => build/Chrome}/background.js | 0 {Chrome => build/Chrome}/chrome-bootstrap.css | 0 {Chrome => build/Chrome}/manifest.json | 0 {Chrome => build/Chrome}/options.js | 0 {Firefox => build/Firefox}/lib/main.js | 0 {Firefox => build/Firefox}/lib/settings.js | 0 {Firefox => build/Firefox}/package.json | 0 {Firefox => build/Firefox}/test/test-main.js | 0 build/README.txt | 3 + .../Safari.safariextension}/.gitignore | 0 .../Safari.safariextension}/Info.plist | 0 .../Safari.safariextension}/background.html | 0 .../Safari.safariextension}/notification.html | 0 .../Safari.safariextension}/notification.png | Bin .../chrome-bootstrap}/.gitignore | 0 .../chrome-bootstrap}/LICENSE | 0 .../chrome-bootstrap}/README.md | 0 .../chrome-bootstrap}/bower.json | 0 .../chrome-bootstrap}/chrome-bootstrap.css | 0 .../chrome-bootstrap}/chrome-bootstrap.less | 0 .../chrome-bootstrap.min.css | 0 .../chrome-bootstrap}/package.json | 0 26 files changed, 58 insertions(+), 55 deletions(-) rename {Chrome => build/Chrome}/.gitignore (100%) rename {Chrome => build/Chrome}/background.js (100%) rename {Chrome => build/Chrome}/chrome-bootstrap.css (100%) rename {Chrome => build/Chrome}/manifest.json (100%) rename {Chrome => build/Chrome}/options.js (100%) rename {Firefox => build/Firefox}/lib/main.js (100%) rename {Firefox => build/Firefox}/lib/settings.js (100%) rename {Firefox => build/Firefox}/package.json (100%) rename {Firefox => build/Firefox}/test/test-main.js (100%) create mode 100644 build/README.txt rename {Safari.safariextension => build/Safari.safariextension}/.gitignore (100%) rename {Safari.safariextension => build/Safari.safariextension}/Info.plist (100%) rename {Safari.safariextension => build/Safari.safariextension}/background.html (100%) rename {Safari.safariextension => build/Safari.safariextension}/notification.html (100%) rename {Safari.safariextension => build/Safari.safariextension}/notification.png (100%) rename {chrome-bootstrap => build/chrome-bootstrap}/.gitignore (100%) rename {chrome-bootstrap => build/chrome-bootstrap}/LICENSE (100%) rename {chrome-bootstrap => build/chrome-bootstrap}/README.md (100%) rename {chrome-bootstrap => build/chrome-bootstrap}/bower.json (100%) rename {chrome-bootstrap => build/chrome-bootstrap}/chrome-bootstrap.css (100%) rename {chrome-bootstrap => build/chrome-bootstrap}/chrome-bootstrap.less (100%) rename {chrome-bootstrap => build/chrome-bootstrap}/chrome-bootstrap.min.css (100%) rename {chrome-bootstrap => build/chrome-bootstrap}/package.json (100%) diff --git a/.gitignore b/.gitignore index bced4ff..22652d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ -/firefox-unpacked -/firefox-addon-sdk-url.txt -/firefox-addon-sdk +/build/firefox-unpacked +/build/firefox-addon-sdk-url.txt +/build/firefox-addon-sdk /lib/local_settings.json -/Chrome.pem -/build +/build/Chrome.pem +/out *~ -/Chrome/*.png -/Chrome/options.html -/Firefox/*.png -/Firefox/data/*.js -/Safari.safariextension/Settings.plist +/build/Chrome/*.png +/build/Chrome/options.html +/build/Firefox/*.png +/build/Firefox/data/*.js +/build/Safari.safariextension/Settings.plist diff --git a/README.md b/README.md index 55e092c..3978dbd 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ recognized by Safari. Don't remove that from the name! - Check "Developer Mode" -- Click "load unpacked extension" and choose the Chrome directory +- Click "load unpacked extension" and choose the build/Chrome directory - You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/) @@ -106,7 +106,7 @@ recognized by Safari. Don't remove that from the name! - Open the "extensions" subdirectory and look for a subdirectory matching the "id" in your settings.json file -- Delete the file and replace it with a link to your extension's "firefox-unpacked" directory +- Delete the file and replace it with a link to your extension's "build/firefox-unpacked" directory - Restart Firefox @@ -124,7 +124,7 @@ recognized by Safari. Don't remove that from the name! - Click the + button at the bottom left, and choose "Add Extension" -- Choose the Safari.safariextension folder from BabelExt +- Choose the build/Safari.safariextension folder from BabelExt - You're good to go! If you just want to try out the BabelExt kitchen sink demo, navigate to [http://babelext.com/demo/](http://babelext.com/demo/) diff --git a/bin/build.js b/bin/build.js index dbde5ad..2d828d1 100755 --- a/bin/build.js +++ b/bin/build.js @@ -409,7 +409,7 @@ function build_safari() { 'late' : 'End' }; - var document = new DOMParser().parseFromString(fs.read('Safari.safariextension/Info.plist'),"text/xml"); + var document = new DOMParser().parseFromString(fs.read('build/Safari.safariextension/Info.plist'),"text/xml"); function get_node( key ) { return document @@ -449,7 +449,7 @@ function build_safari() { while ( end_scripts.firstChild) end_scripts.removeChild( end_scripts.firstChild); settings.contentScriptFiles.forEach(function(file) { - hardLink( 'lib/'+file, 'Safari.safariextension/' + file ) + hardLink( 'lib/'+file, 'build/Safari.safariextension/' + file ) var script = document.createElement("string"); script.textContent = file; @@ -468,7 +468,7 @@ function build_safari() { end_scripts.appendChild( document.createTextNode('\n\t\t\t') ); var xml_txt = '\n' + new XMLSerializer().serializeToString(document).replace(">",">\n") + "\n"; - fs.write( 'Safari.safariextension/Info.plist', xml_txt ); + fs.write( 'build/Safari.safariextension/Info.plist', xml_txt ); if ( settings.preferences ) function build_dict( preference, values ) { @@ -482,7 +482,7 @@ function build_safari() { '\n\t\n' } fs.write( - 'Safari.safariextension/Settings.plist', + 'build/Safari.safariextension/Settings.plist', '\n' + '\n' + '\n' + @@ -514,7 +514,7 @@ function build_firefox() { // Create settings.js: fs.write( - 'Firefox/lib/settings.js', + 'build/Firefox/lib/settings.js', ( settings.match_secure_domain ? 'exports.include = ["http://' + settings.match_domain + '/*","https://' + settings.match_domain + '/*"];\n' : 'exports.include = ["http://' + settings.match_domain + '/*"];\n' @@ -535,15 +535,15 @@ function build_firefox() { "id": settings.id, "name": settings.name }; - if (settings.icons[48] ) { pkg.icon = settings.icons[48]; symbolicLink( '../lib/'+pkg.icon , 'Firefox/'+pkg.icon ); } - if (settings.icons[64] ) { pkg.icon_64 = settings.icons[64]; symbolicLink( '../lib/'+pkg.icon_64, 'Firefox/'+pkg.icon_64 ); } + if (settings.icons[48] ) { pkg.icon = settings.icons[48]; symbolicLink( '../lib/'+pkg.icon , 'build/Firefox/'+pkg.icon ); } + if (settings.icons[64] ) { pkg.icon_64 = settings.icons[64]; symbolicLink( '../lib/'+pkg.icon_64, 'build/Firefox/'+pkg.icon_64 ); } if (settings.preferences) { pkg.preferences = settings.preferences; } - fs.write( 'Firefox/package.json', JSON.stringify(pkg, null, ' ' ) + "\n", 'w' ); + fs.write( 'build/Firefox/package.json', JSON.stringify(pkg, null, ' ' ) + "\n", 'w' ); // Copy scripts into place: - fs.removeTree('Firefox/data'); // PhantomJS won't list dangling symlinks, so we have to just delete the directory and recreate it - fs.makeDirectory('Firefox/data'); - settings.contentScriptFiles.forEach(function(file) { symbolicLink( '../../lib/'+file, 'Firefox/data/' + file ) }); + fs.removeTree('build/Firefox/data'); // PhantomJS won't list dangling symlinks, so we have to just delete the directory and recreate it + fs.makeDirectory('build/Firefox/data'); + settings.contentScriptFiles.forEach(function(file) { symbolicLink( '../../lib/'+file, 'build/Firefox/data/' + file ) }); program_counter.begin(); @@ -565,11 +565,11 @@ function build_firefox() { console.log( 'Unpacking Firefox Addon SDK...', status ); childProcess.execFile( 'curl', ['--silent',response.redirectURL,'-o','temporary_file.tar.gz'], null, function(err, stdout, stderr) { if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } - fs.makeDirectory('firefox-addon-sdk'); - childProcess.execFile( 'tar', ["zxf",'temporary_file.tar.gz','-C','firefox-addon-sdk','--strip-components=1'], null, function(err,stdout,stderr) { + fs.makeDirectory('build/firefox-addon-sdk'); + childProcess.execFile( 'tar', ["zxf",'temporary_file.tar.gz','-C','build/firefox-addon-sdk','--strip-components=1'], null, function(err,stdout,stderr) { if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } fs.remove('temporary_file.tar.gz'); - fs.write( 'firefox-addon-sdk-url.txt', response.redirectURL, 'w' ); + fs.write( 'build/firefox-addon-sdk-url.txt', response.redirectURL, 'w' ); build_xpi(); }); }); @@ -583,19 +583,19 @@ function build_firefox() { function build_xpi() { if ( system.os.name == 'windows' ) { // TODO: fill in real Windows values here (the following line is just a guess): - childProcess.execFile( 'cmd' , [ 'cd firefox-addon-sdk ; bin\activate ; cd ../Firefox ; cfx xpi'], null, finalise_xpi ); + childProcess.execFile( 'cmd' , [ 'cd build\firefox-addon-sdk ; bin\activate ; cd ../Firefox ; cfx xpi'], null, finalise_xpi ); } else { - childProcess.execFile( 'bash', ['-c','cd firefox-addon-sdk && source bin/activate && cd ../Firefox && cfx xpi'], null, finalise_xpi ); + childProcess.execFile( 'bash', ['-c','cd build/firefox-addon-sdk && source bin/activate && cd ../Firefox && cfx xpi'], null, finalise_xpi ); } } // Move the .xpi into place, fix its install.rdf, and update firefox-unpacked: function finalise_xpi(err, stdout, stderr) { if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } - fs.makeDirectory('build'); - var xpi = 'build/' + settings.name + '.xpi'; + fs.makeDirectory('out'); + var xpi = 'out/' + settings.name + '.xpi'; if ( fs.exists(xpi) ) fs.remove(xpi); - fs.list('Firefox').forEach(function(file) { if ( file.search(/\.xpi$/) != -1 ) fs.move( 'Firefox/' + file, xpi ); }); + fs.list('build/Firefox').forEach(function(file) { if ( file.search(/\.xpi$/) != -1 ) fs.move( 'build/Firefox/' + file, xpi ); }); fs.removeTree('firefox-unpacked'); fs.makeDirectory('firefox-unpacked'); childProcess.execFile( 'unzip', ['-d','firefox-unpacked',xpi], null, function(err,stdout,stderr) { @@ -662,17 +662,17 @@ function build_chrome() { manifest.options_page = "options.html"; manifest.permissions.push('storage'); manifest.background.scripts.unshift('preferences.js'); - extra_files.push('Chrome/'+manifest.background.scripts[0]); - extra_files.push('Chrome/'+manifest.options_page); + extra_files.push('build/Chrome/'+manifest.background.scripts[0]); + extra_files.push('build/Chrome/'+manifest.options_page); - fs.list('Chrome').forEach(function(file) { + fs.list('build/Chrome').forEach(function(file) { if ( file[0] == '.' ) return; if ( file.search( /^(?:background\.js|chrome-bootstrap\.css|options\.js)$/ ) == 0 ) return; - fs.remove('Chrome/' + file); + fs.remove('build/Chrome/' + file); }); fs.write( - 'Chrome/' + manifest.background.scripts[0], + 'build/Chrome/' + manifest.background.scripts[0], "var default_preferences = {" + settings.preferences.map(function(preference) { switch ( preference.type ) { @@ -686,7 +686,7 @@ function build_chrome() { ); fs.write( - 'Chrome/' + manifest.options_page, + 'build/Chrome/' + manifest.options_page, "\n" + "\n" + "" + settings.title + " Options\n" + @@ -730,19 +730,19 @@ function build_chrome() { } // Create manifest.json: - fs.write( 'Chrome/manifest.json', JSON.stringify(manifest, null, '\t' ) + "\n", 'w' ); + fs.write( 'build/Chrome/manifest.json', JSON.stringify(manifest, null, '\t' ) + "\n", 'w' ); // Copy scripts and icons into place: - settings.contentScriptFiles.forEach(function(file) { hardLink( 'lib/'+file , 'Chrome/' + file ) }); - Object.keys(settings.icons).forEach(function(key ) { hardLink( 'lib/'+settings.icons[key], 'Chrome/' + settings.icons[key] ) }); + settings.contentScriptFiles.forEach(function(file) { hardLink( 'lib/'+file , 'build/Chrome/' + file ) }); + Object.keys(settings.icons).forEach(function(key ) { hardLink( 'lib/'+settings.icons[key], 'build/Chrome/' + settings.icons[key] ) }); program_counter.begin(); // Create a Chrome key: - if (fs.exists('Chrome.pem')) { + if (fs.exists('build/Chrome.pem')) { build_crx(); } else { - childProcess.execFile(chrome_command, ["--pack-extension=Chrome"], null, function (err, stdout, stderr) { + childProcess.execFile(chrome_command, ["--pack-extension=build/Chrome"], null, function (err, stdout, stderr) { if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } build_crx(); }); @@ -750,25 +750,25 @@ function build_chrome() { // Build the .crx, move it into place, and build the upload zip file: function build_crx() { - childProcess.execFile(chrome_command, ["--pack-extension=Chrome","--pack-extension-key=Chrome.pem"], null, function (err, stdout, stderr) { + childProcess.execFile(chrome_command, ["--pack-extension=build/Chrome","--pack-extension-key=build/Chrome.pem"], null, function (err, stdout, stderr) { if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } - if ( stdout != 'Created the extension:\n\nChrome.crx\n' ) console.log(stdout.replace(/\n$/,'')); - var crx = 'build/' + settings.name + '.crx'; + if ( stdout != 'Created the extension:\n\nbuild/Chrome.crx\n' ) console.log(stdout.replace(/\n$/,'')); + var crx = 'out/' + settings.name + '.crx'; if ( fs.exists(crx) ) fs.remove(crx); - fs.move( 'Chrome.crx', crx ); + fs.move( 'build/Chrome.crx', crx ); console.log('Built ' + crx); - if ( fs.exists('build/chrome-store-upload.zip') ) fs.remove('build/chrome-store-upload.zip'); + if ( fs.exists('out/chrome-store-upload.zip') ) fs.remove('out/chrome-store-upload.zip'); childProcess.execFile( 'zip', - ['build/chrome-store-upload.zip','Chrome/background.js','Chrome/manifest.json'] + ['out/chrome-store-upload.zip','build/Chrome/background.js','build/Chrome/manifest.json'] .concat( extra_files ) - .concat( settings.contentScriptFiles.map(function(file) { return 'Chrome/'+file }) ) - .concat( Object.keys(settings.icons).map(function(key ) { return 'Chrome/' + settings.icons[key] }) ) + .concat( settings.contentScriptFiles.map(function(file) { return 'build/Chrome/'+file }) ) + .concat( Object.keys(settings.icons).map(function(key ) { return 'build/Chrome/' + settings.icons[key] }) ) , null, function(err,stdout,stderr) { if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } - console.log('Built build/chrome-store-upload.zip'); + console.log('Built out/chrome-store-upload.zip'); return program_counter.end(0); } ); @@ -903,7 +903,7 @@ function release_amo(login_info) { page.submit_form( '#upload-addon', { - '#upload-addon': 'build/' + settings.name + '.xpi' + '#upload-addon': 'out/' + settings.name + '.xpi' }, function() { page.waitForElementsPresent( @@ -1055,7 +1055,7 @@ function release_chrome(login_info) { page.open( "https://www.googleapis.com/upload/chromewebstore/v1.1/items/" + login_info.id, 'PUT', - fs.open('build/chrome-store-upload.zip', 'rb').read(), + fs.open('out/chrome-store-upload.zip', 'rb').read(), function (status) { if ( status == "success" ) { var result = JSON.parse(page.plainText); @@ -1118,7 +1118,7 @@ function release_opera(login_info) { page.submit_form( '.submit-button', { - '#id_package_file': 'build/' + settings.name + '.crx' + '#id_package_file': 'out/' + settings.name + '.crx' }, function() { page.submit_form( diff --git a/Chrome/.gitignore b/build/Chrome/.gitignore similarity index 100% rename from Chrome/.gitignore rename to build/Chrome/.gitignore diff --git a/Chrome/background.js b/build/Chrome/background.js similarity index 100% rename from Chrome/background.js rename to build/Chrome/background.js diff --git a/Chrome/chrome-bootstrap.css b/build/Chrome/chrome-bootstrap.css similarity index 100% rename from Chrome/chrome-bootstrap.css rename to build/Chrome/chrome-bootstrap.css diff --git a/Chrome/manifest.json b/build/Chrome/manifest.json similarity index 100% rename from Chrome/manifest.json rename to build/Chrome/manifest.json diff --git a/Chrome/options.js b/build/Chrome/options.js similarity index 100% rename from Chrome/options.js rename to build/Chrome/options.js diff --git a/Firefox/lib/main.js b/build/Firefox/lib/main.js similarity index 100% rename from Firefox/lib/main.js rename to build/Firefox/lib/main.js diff --git a/Firefox/lib/settings.js b/build/Firefox/lib/settings.js similarity index 100% rename from Firefox/lib/settings.js rename to build/Firefox/lib/settings.js diff --git a/Firefox/package.json b/build/Firefox/package.json similarity index 100% rename from Firefox/package.json rename to build/Firefox/package.json diff --git a/Firefox/test/test-main.js b/build/Firefox/test/test-main.js similarity index 100% rename from Firefox/test/test-main.js rename to build/Firefox/test/test-main.js diff --git a/build/README.txt b/build/README.txt new file mode 100644 index 0000000..f8a2c04 --- /dev/null +++ b/build/README.txt @@ -0,0 +1,3 @@ +Each web browser wants you to build your extension in a slightly different way. + +The build script will automatically build browser-specific extensions from the values you provide. diff --git a/Safari.safariextension/.gitignore b/build/Safari.safariextension/.gitignore similarity index 100% rename from Safari.safariextension/.gitignore rename to build/Safari.safariextension/.gitignore diff --git a/Safari.safariextension/Info.plist b/build/Safari.safariextension/Info.plist similarity index 100% rename from Safari.safariextension/Info.plist rename to build/Safari.safariextension/Info.plist diff --git a/Safari.safariextension/background.html b/build/Safari.safariextension/background.html similarity index 100% rename from Safari.safariextension/background.html rename to build/Safari.safariextension/background.html diff --git a/Safari.safariextension/notification.html b/build/Safari.safariextension/notification.html similarity index 100% rename from Safari.safariextension/notification.html rename to build/Safari.safariextension/notification.html diff --git a/Safari.safariextension/notification.png b/build/Safari.safariextension/notification.png similarity index 100% rename from Safari.safariextension/notification.png rename to build/Safari.safariextension/notification.png diff --git a/chrome-bootstrap/.gitignore b/build/chrome-bootstrap/.gitignore similarity index 100% rename from chrome-bootstrap/.gitignore rename to build/chrome-bootstrap/.gitignore diff --git a/chrome-bootstrap/LICENSE b/build/chrome-bootstrap/LICENSE similarity index 100% rename from chrome-bootstrap/LICENSE rename to build/chrome-bootstrap/LICENSE diff --git a/chrome-bootstrap/README.md b/build/chrome-bootstrap/README.md similarity index 100% rename from chrome-bootstrap/README.md rename to build/chrome-bootstrap/README.md diff --git a/chrome-bootstrap/bower.json b/build/chrome-bootstrap/bower.json similarity index 100% rename from chrome-bootstrap/bower.json rename to build/chrome-bootstrap/bower.json diff --git a/chrome-bootstrap/chrome-bootstrap.css b/build/chrome-bootstrap/chrome-bootstrap.css similarity index 100% rename from chrome-bootstrap/chrome-bootstrap.css rename to build/chrome-bootstrap/chrome-bootstrap.css diff --git a/chrome-bootstrap/chrome-bootstrap.less b/build/chrome-bootstrap/chrome-bootstrap.less similarity index 100% rename from chrome-bootstrap/chrome-bootstrap.less rename to build/chrome-bootstrap/chrome-bootstrap.less diff --git a/chrome-bootstrap/chrome-bootstrap.min.css b/build/chrome-bootstrap/chrome-bootstrap.min.css similarity index 100% rename from chrome-bootstrap/chrome-bootstrap.min.css rename to build/chrome-bootstrap/chrome-bootstrap.min.css diff --git a/chrome-bootstrap/package.json b/build/chrome-bootstrap/package.json similarity index 100% rename from chrome-bootstrap/package.json rename to build/chrome-bootstrap/package.json From 9171477ca6686a4ea9eac11fa05dd549dd82c932 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sat, 14 Feb 2015 01:13:58 +0000 Subject: [PATCH 056/105] Renamed "bin/" to "script/" --- README.md | 4 ++-- {bin => script}/build.js | 0 {bin => script}/build.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename {bin => script}/build.js (100%) rename {bin => script}/build.sh (95%) diff --git a/README.md b/README.md index 3978dbd..3bdf0e3 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ First, download all of the source from Github and put it together within a folde Then, download PhantomJS (http://phantomjs.org), which is used to build and deploy extensions. -In UNIX-based OSes, run `./bin/build.sh build` to build packages, and `./bin/build.sh release` +In UNIX-based OSes, run `./script/build.sh build` to build packages, and `./script/build.sh release` to release them. The build system hasn't been tested under Windows yet - your best bet is probably to look at @@ -146,5 +146,5 @@ extension data by deleting all files matching /Local*/* Date: Sat, 14 Feb 2015 01:15:57 +0000 Subject: [PATCH 057/105] Moved configuration to conf/ --- .gitignore | 2 +- {lib => conf}/local_settings.json.example | 0 {lib => conf}/settings.json | 0 script/build.sh | 6 +++--- 4 files changed, 4 insertions(+), 4 deletions(-) rename {lib => conf}/local_settings.json.example (100%) rename {lib => conf}/settings.json (100%) diff --git a/.gitignore b/.gitignore index 22652d7..1cc8d67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /build/firefox-unpacked /build/firefox-addon-sdk-url.txt /build/firefox-addon-sdk -/lib/local_settings.json +/conf/local_settings.json /build/Chrome.pem /out *~ diff --git a/lib/local_settings.json.example b/conf/local_settings.json.example similarity index 100% rename from lib/local_settings.json.example rename to conf/local_settings.json.example diff --git a/lib/settings.json b/conf/settings.json similarity index 100% rename from lib/settings.json rename to conf/settings.json diff --git a/script/build.sh b/script/build.sh index c284f28..85a361d 100755 --- a/script/build.sh +++ b/script/build.sh @@ -2,21 +2,21 @@ if [ "$1" == "release" ] then - if grep -q chrome_info lib/local_settings.json && [ -z "$CHROME_PASSWORD" ] + if grep -q chrome_info conf/local_settings.json && [ -z "$CHROME_PASSWORD" ] then read -p 'Password for chrome.google.com: ' -s CHROME_PASSWORD echo fi export CHROME_PASSWORD - if grep -q amo_info lib/local_settings.json && [ -z "$AMO_PASSWORD" ] + if grep -q amo_info conf/local_settings.json && [ -z "$AMO_PASSWORD" ] then read -p 'Password for addons.mozilla.org: ' -s AMO_PASSWORD echo fi export AMO_PASSWORD - if grep -q opera_info lib/local_settings.json && [ -z "$OPERA_PASSWORD" ] + if grep -q opera_info conf/local_settings.json && [ -z "$OPERA_PASSWORD" ] then read -p 'Password for developer.opera.com: ' -s OPERA_PASSWORD echo From 654ded21e814cb3317fa30e0314d44286f907afa Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sat, 14 Feb 2015 01:17:38 +0000 Subject: [PATCH 058/105] Moved some JS to src/ --- {lib => src}/extension.js | 0 {lib => src}/test-helper.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {lib => src}/extension.js (100%) rename {lib => src}/test-helper.js (100%) diff --git a/lib/extension.js b/src/extension.js similarity index 100% rename from lib/extension.js rename to src/extension.js diff --git a/lib/test-helper.js b/src/test-helper.js similarity index 100% rename from lib/test-helper.js rename to src/test-helper.js From 45d9e315c468d6b27c7ad6a6fdf8744c91b8fbf3 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 15 Feb 2015 08:56:29 +0000 Subject: [PATCH 059/105] Add icons/README.txt --- icons/README.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 icons/README.txt diff --git a/icons/README.txt b/icons/README.txt new file mode 100644 index 0000000..17f51db --- /dev/null +++ b/icons/README.txt @@ -0,0 +1,3 @@ +Put icon files in this directory. + +Ideally, you want a 16x16, 32x32, 48x48, 64x64 and 128x128 icon. From 2419971fbd3e54f15fd8a16e65e633e368e7c2a3 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 15 Feb 2015 08:57:42 +0000 Subject: [PATCH 060/105] Add lib/README.txt --- lib/README.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/README.txt diff --git a/lib/README.txt b/lib/README.txt new file mode 100644 index 0000000..e7a53f7 --- /dev/null +++ b/lib/README.txt @@ -0,0 +1 @@ +Add third-party libraries to this directory. From 3712d8cd7ab8ff0801b22820fc5d9cc478d8ad06 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 15 Feb 2015 09:00:13 +0000 Subject: [PATCH 061/105] Updated build system for directory reshuffle --- .gitignore | 7 ++--- conf/settings.json | 14 ++++----- script/build.js | 78 ++++++++++++++++++++++++++++------------------ 3 files changed, 57 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index 1cc8d67..70a618b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,7 @@ /build/Chrome.pem /out *~ -/build/Chrome/*.png -/build/Chrome/options.html +/build/Chrome /build/Firefox/*.png -/build/Firefox/data/*.js -/build/Safari.safariextension/Settings.plist +/build/Firefox/data +/build/Safari.safariextension diff --git a/conf/settings.json b/conf/settings.json index 2302e45..80447c9 100644 --- a/conf/settings.json +++ b/conf/settings.json @@ -16,11 +16,11 @@ "website": "http://babelext.com", "icons": { /* - 16: "icon-16.png", - 32: "icon-32.png", - 48: "icon-48.png", - 64: "icon-64.png", - 128: "icon-128.png" + 16: "icons/icon-16.png", + 32: "icons/icon-32.png", + 48: "icons/icon-48.png", + 64: "icons/icon-64.png", + 128: "icons/icon-128.png" */ }, @@ -39,7 +39,7 @@ */ "contentScriptWhen": "middle", - "contentScriptFiles": [ "extension.js" ], + "contentScriptFiles": [ "src/extension.js" ], "match_domain": "babelext.com", // or *.babelext.com to include subdomains // whether to match https://: "match_secure_domain": true, @@ -54,7 +54,7 @@ }, "test": { - "contentScriptFiles": [ "test-helper.js" ], + "contentScriptFiles": [ "src/test-helper.js" ], }, "production": { diff --git a/script/build.js b/script/build.js index 2d828d1..e216ce5 100755 --- a/script/build.js +++ b/script/build.js @@ -42,6 +42,7 @@ phantom.onError = function(msg, trace) { * Create a symbolic link from source to target */ function symbolicLink( source, target ) { + target.replace(/\//g, function() { source = '../' + source }); if ( ! fs.isLink(target) ) { if ( system.os.name == 'windows' ) { childProcess.execFile('mklink', [target,source], function(err, stdout, stderr) { @@ -291,14 +292,14 @@ AsyncCounter.prototype.end = function(errors) { this.errors += (errors||0); if var program_counter = new AsyncCounter(function(errors) { phantom.exit(errors||0) }); /* - * Load settings from lib/settings.json + * Load settings from conf/settings.json */ var settings; try { - settings = eval('('+fs.read('lib/settings.json')+')'); + settings = eval('('+fs.read('conf/settings.json')+')'); } catch (e) { console.error( - "Error in lib/settings.json: " + e + "\n" + + "Error in conf/settings.json: " + e + "\n" + "Please make sure the file is formatted correctly and try again." ); phantom.exit(1); @@ -327,7 +328,7 @@ if ( system.env.hasOwnProperty('ENVIRONMENT') ) { ); phantom.exit(1); }; -settings.contentScriptFiles.unshift('BabelExt.js'); +settings.contentScriptFiles.unshift('lib/BabelExt.js'); delete settings.environment_specific; if ( @@ -360,14 +361,14 @@ settings.preferences.forEach(function(preference) { /* - * Load settings from lib/local_settings.json + * Load settings from conf/local_settings.json */ var local_settings; try { - local_settings = eval('('+fs.read('lib/local_settings.json')+')'); + local_settings = eval('('+fs.read('conf/local_settings.json')+')'); } catch (e) { console.error( - "Error in lib/local_settings.json: " + e + "\n" + + "Error in conf/local_settings.json: " + e + "\n" + "Please make sure the file is formatted correctly and try again." ); phantom.exit(1); @@ -448,14 +449,18 @@ function build_safari() { while (start_scripts.firstChild) start_scripts.removeChild(start_scripts.firstChild); while ( end_scripts.firstChild) end_scripts.removeChild( end_scripts.firstChild); + fs.makeDirectory('build/Safari.safariextension/icons'); + fs.makeDirectory('build/Safari.safariextension/src'); + fs.makeDirectory('build/Safari.safariextension/lib'); + settings.contentScriptFiles.forEach(function(file) { - hardLink( 'lib/'+file, 'build/Safari.safariextension/' + file ) + hardLink( file, 'build/Safari.safariextension/' + file ) var script = document.createElement("string"); script.textContent = file; - if ( file == 'BabelExt.js' || when_string[ settings.contentScriptWhen ] == 'Start' ) { + if ( file == 'lib/BabelExt.js' || when_string[ settings.contentScriptWhen ] == 'Start' ) { start_scripts.appendChild( document.createTextNode('\n\t\t\t\t') ); start_scripts.appendChild(script); } else { @@ -512,6 +517,15 @@ function build_firefox() { 'late' : 'end' }; + // Copy scripts into place: + fs.removeTree('build/Firefox/data'); // PhantomJS won't list dangling symlinks, so we have to just delete the directory and recreate it + fs.makeDirectory('build/Firefox/data'); + fs.makeDirectory('build/Firefox/data/src'); + fs.makeDirectory('build/Firefox/data/lib'); + fs.makeDirectory('build/Firefox/icons'); + + settings.contentScriptFiles.forEach(function(file) { symbolicLink( file, 'build/Firefox/data/' + file ) }); + // Create settings.js: fs.write( 'build/Firefox/lib/settings.js', @@ -535,16 +549,11 @@ function build_firefox() { "id": settings.id, "name": settings.name }; - if (settings.icons[48] ) { pkg.icon = settings.icons[48]; symbolicLink( '../lib/'+pkg.icon , 'build/Firefox/'+pkg.icon ); } - if (settings.icons[64] ) { pkg.icon_64 = settings.icons[64]; symbolicLink( '../lib/'+pkg.icon_64, 'build/Firefox/'+pkg.icon_64 ); } + if (settings.icons[48] ) { pkg.icon = settings.icons[48]; symbolicLink( pkg.icon , 'build/Firefox/'+pkg.icon ); } + if (settings.icons[64] ) { pkg.icon_64 = settings.icons[64]; symbolicLink( pkg.icon_64, 'build/Firefox/'+pkg.icon_64 ); } if (settings.preferences) { pkg.preferences = settings.preferences; } fs.write( 'build/Firefox/package.json', JSON.stringify(pkg, null, ' ' ) + "\n", 'w' ); - // Copy scripts into place: - fs.removeTree('build/Firefox/data'); // PhantomJS won't list dangling symlinks, so we have to just delete the directory and recreate it - fs.makeDirectory('build/Firefox/data'); - settings.contentScriptFiles.forEach(function(file) { symbolicLink( '../../lib/'+file, 'build/Firefox/data/' + file ) }); - program_counter.begin(); // Check whether the Addon SDK is up-to-date: @@ -555,7 +564,7 @@ function build_firefox() { return program_counter.end(1); }; page.onResourceReceived = function(response) { - if ( fs.exists('firefox-addon-sdk-url.txt') && fs.read('firefox-addon-sdk-url.txt') == response.redirectURL ) { + if ( fs.exists('build/firefox-addon-sdk-url.txt') && fs.read('build/firefox-addon-sdk-url.txt') == response.redirectURL ) { console.log( 'Firefox Addon SDK is up-to-date.' ); build_xpi(); } else { @@ -596,19 +605,19 @@ function build_firefox() { var xpi = 'out/' + settings.name + '.xpi'; if ( fs.exists(xpi) ) fs.remove(xpi); fs.list('build/Firefox').forEach(function(file) { if ( file.search(/\.xpi$/) != -1 ) fs.move( 'build/Firefox/' + file, xpi ); }); - fs.removeTree('firefox-unpacked'); - fs.makeDirectory('firefox-unpacked'); - childProcess.execFile( 'unzip', ['-d','firefox-unpacked',xpi], null, function(err,stdout,stderr) { + fs.removeTree('build/firefox-unpacked'); + fs.makeDirectory('build/firefox-unpacked'); + childProcess.execFile( 'unzip', ['-d','build/firefox-unpacked',xpi], null, function(err,stdout,stderr) { if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } fs.write( - 'firefox-unpacked/install.rdf', - fs.read('firefox-unpacked/install.rdf').replace( /.*<\/em:maxVersion>/, '' + settings.firefox_max_version + '' ) + 'build/firefox-unpacked/install.rdf', + fs.read('build/firefox-unpacked/install.rdf').replace( /.*<\/em:maxVersion>/, '' + settings.firefox_max_version + '' ) ); settings.contentScriptFiles.forEach(function(file) { - fs.remove('firefox-unpacked/resources/'+settings.name+'/data/'+file); - symbolicLink( '../../../../lib/'+file, 'firefox-unpacked/resources/'+settings.name+'/data/'+file ) + fs.remove('build/firefox-unpacked/resources/'+settings.name+'/data/'+file); + symbolicLink( file, 'build/firefox-unpacked/resources/'+settings.name+'/data/'+file ) }); - fs.changeWorkingDirectory('firefox-unpacked'); + fs.changeWorkingDirectory('build/firefox-unpacked'); childProcess.execFile( 'zip', ['../'+xpi,'install.rdf'], null, function(err,stdout,stderr) { fs.changeWorkingDirectory('..'); if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } @@ -668,7 +677,10 @@ function build_chrome() { fs.list('build/Chrome').forEach(function(file) { if ( file[0] == '.' ) return; if ( file.search( /^(?:background\.js|chrome-bootstrap\.css|options\.js)$/ ) == 0 ) return; - fs.remove('build/Chrome/' + file); + if ( fs.isDirectory(file) ) + fs.removeTree('build/Chrome/' + file); + else + fs.remove ('build/Chrome/' + file); }); fs.write( @@ -732,9 +744,13 @@ function build_chrome() { // Create manifest.json: fs.write( 'build/Chrome/manifest.json', JSON.stringify(manifest, null, '\t' ) + "\n", 'w' ); + fs.makeDirectory('build/Chrome/icons'); + fs.makeDirectory('build/Chrome/src'); + fs.makeDirectory('build/Chrome/lib'); + // Copy scripts and icons into place: - settings.contentScriptFiles.forEach(function(file) { hardLink( 'lib/'+file , 'build/Chrome/' + file ) }); - Object.keys(settings.icons).forEach(function(key ) { hardLink( 'lib/'+settings.icons[key], 'build/Chrome/' + settings.icons[key] ) }); + settings.contentScriptFiles.forEach(function(file) { hardLink( file , 'build/Chrome/' + file ) }); + Object.keys(settings.icons).forEach(function(key ) { hardLink( settings.icons[key], 'build/Chrome/' + settings.icons[key] ) }); program_counter.begin(); @@ -875,7 +891,7 @@ function release_amo(login_info) { page.submit_form( '#id_icon_upload', { - '#id_icon_upload': 'lib/'+best_icon + '#id_icon_upload': best_icon }, function() { setTimeout(function() { @@ -970,7 +986,7 @@ function release_chrome(login_info) { page.submit_form( '.id-upload-image.cx-bold', { - '#cx-img-uploader-input': 'lib/' + settings.icons[128] + '#cx-img-uploader-input': settings.icons[128] }, function() { page.waitForElementsPresent( @@ -1128,7 +1144,7 @@ function release_opera(login_info) { '#id_translations-0-long_description' : settings.long_description, '#id_translations-0-changelog' : changelog, '#id_target_platform-comment' : login_info.tested_on, - '#id_icons-0-icon' : settings.icons[64] ? 'lib/' + settings.icons[64] : undefined, + '#id_icons-0-icon' : settings.icons[64] ? settings.icons[64] : undefined, }, function() { page.click('input.submit-button[type="submit"][name="approve_widget"]'); From 640a9a9409c924548e2d2ad317640d41a0a6abf8 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 15 Feb 2015 09:54:39 +0000 Subject: [PATCH 062/105] Support stylesheets --- build/Firefox/lib/main.js | 1 + build/Safari.safariextension/Info.plist | 4 +++ conf/settings.json | 1 + script/build.js | 41 ++++++++++++++++++++----- src/extension.css | 3 ++ 5 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 src/extension.css diff --git a/build/Firefox/lib/main.js b/build/Firefox/lib/main.js index b339467..48558b6 100644 --- a/build/Firefox/lib/main.js +++ b/build/Firefox/lib/main.js @@ -50,6 +50,7 @@ pageMod.PageMod({ include: settings.include, contentScriptWhen: settings.contentScriptWhen, contentScriptFile: settings.contentScriptFile.map(function(file) { return self.data.url(file) }), + contentStyleFile: settings.contentStyleFile.map(function(file) { return self.data.url(file) }), onAttach: function(worker) { tabs.on('activate', function(tab) { // run some code when a tab is activated... diff --git a/build/Safari.safariextension/Info.plist b/build/Safari.safariextension/Info.plist index ed3327c..23f333b 100644 --- a/build/Safari.safariextension/Info.plist +++ b/build/Safari.safariextension/Info.plist @@ -64,6 +64,10 @@ BabelExt.js + Stylesheets + + src/extension.css + Description An extension created with BabelExt - www.babelext.com diff --git a/conf/settings.json b/conf/settings.json index 80447c9..f439494 100644 --- a/conf/settings.json +++ b/conf/settings.json @@ -40,6 +40,7 @@ "contentScriptWhen": "middle", "contentScriptFiles": [ "src/extension.js" ], + "contentStyleFiles": [ "src/extension.css" ], "match_domain": "babelext.com", // or *.babelext.com to include subdomains // whether to match https://: "match_secure_domain": true, diff --git a/script/build.js b/script/build.js index e216ce5..9d7f952 100755 --- a/script/build.js +++ b/script/build.js @@ -459,7 +459,6 @@ function build_safari() { var script = document.createElement("string"); script.textContent = file; - if ( file == 'lib/BabelExt.js' || when_string[ settings.contentScriptWhen ] == 'Start' ) { start_scripts.appendChild( document.createTextNode('\n\t\t\t\t') ); start_scripts.appendChild(script); @@ -472,6 +471,22 @@ function build_safari() { start_scripts.appendChild( document.createTextNode('\n\t\t\t') ); end_scripts.appendChild( document.createTextNode('\n\t\t\t') ); + var stylesheets = get_node('Stylesheets'); + + while (stylesheets.firstChild) stylesheets.removeChild(stylesheets.firstChild); + + settings.contentStyleFiles.forEach(function(file) { + hardLink( file, 'build/Safari.safariextension/' + file ) + + var sheet = document.createElement("string"); + sheet.textContent = file; + + stylesheets.appendChild( document.createTextNode('\n\t\t\t') ); + stylesheets.appendChild(sheet); + }); + + stylesheets.appendChild( document.createTextNode('\n\t\t') ); + var xml_txt = '\n' + new XMLSerializer().serializeToString(document).replace(">",">\n") + "\n"; fs.write( 'build/Safari.safariextension/Info.plist', xml_txt ); @@ -524,7 +539,9 @@ function build_firefox() { fs.makeDirectory('build/Firefox/data/lib'); fs.makeDirectory('build/Firefox/icons'); - settings.contentScriptFiles.forEach(function(file) { symbolicLink( file, 'build/Firefox/data/' + file ) }); + var contentFiles = settings.contentScriptFiles.concat( settings.contentStyleFiles || [] ); + + contentFiles.forEach(function(file) { symbolicLink( file, 'build/Firefox/data/' + file ) }); // Create settings.js: fs.write( @@ -534,7 +551,8 @@ function build_firefox() { : 'exports.include = ["http://' + settings.match_domain + '/*"];\n' ) + 'exports.contentScriptWhen = "' + when_string[settings.contentScriptWhen] + '";\n' + - 'exports.contentScriptFile = ' + JSON.stringify(settings.contentScriptFiles) + ";\n" + 'exports.contentScriptFile = ' + JSON.stringify(settings.contentScriptFiles) + ";\n" + + 'exports.contentStyleFile = ' + JSON.stringify(settings.contentStyleFiles || []) + ";\n" , 'w' ); @@ -613,7 +631,7 @@ function build_firefox() { 'build/firefox-unpacked/install.rdf', fs.read('build/firefox-unpacked/install.rdf').replace( /.*<\/em:maxVersion>/, '' + settings.firefox_max_version + '' ) ); - settings.contentScriptFiles.forEach(function(file) { + contentFiles.forEach(function(file) { fs.remove('build/firefox-unpacked/resources/'+settings.name+'/data/'+file); symbolicLink( file, 'build/firefox-unpacked/resources/'+settings.name+'/data/'+file ) }); @@ -665,6 +683,15 @@ function build_chrome() { ] }; + var contentFiles = settings.contentScriptFiles.concat( + Object.keys(settings.icons).map(function(key) { return settings.icons[key] }) + ); + if ( settings.contentStyleFiles ) { + manifest.content_scripts[0].css = settings.contentStyleFiles; + contentFiles = contentFiles.concat( settings.contentStyleFiles ); + } + + var extra_files = []; if ( settings.preferences ) { @@ -749,8 +776,7 @@ function build_chrome() { fs.makeDirectory('build/Chrome/lib'); // Copy scripts and icons into place: - settings.contentScriptFiles.forEach(function(file) { hardLink( file , 'build/Chrome/' + file ) }); - Object.keys(settings.icons).forEach(function(key ) { hardLink( settings.icons[key], 'build/Chrome/' + settings.icons[key] ) }); + contentFiles.forEach(function(file) { hardLink( file , 'build/Chrome/' + file ) }); program_counter.begin(); @@ -778,8 +804,7 @@ function build_chrome() { 'zip', ['out/chrome-store-upload.zip','build/Chrome/background.js','build/Chrome/manifest.json'] .concat( extra_files ) - .concat( settings.contentScriptFiles.map(function(file) { return 'build/Chrome/'+file }) ) - .concat( Object.keys(settings.icons).map(function(key ) { return 'build/Chrome/' + settings.icons[key] }) ) + .concat( contentFiles.map(function(file) { return 'build/Chrome/'+file }) ) , null, function(err,stdout,stderr) { diff --git a/src/extension.css b/src/extension.css new file mode 100644 index 0000000..f93ec48 --- /dev/null +++ b/src/extension.css @@ -0,0 +1,3 @@ +/* + * CSS used in your extension + */ \ No newline at end of file From 86aa6e06096cf2a94ef24a97b1ebdc567f7fdc93 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 15 Feb 2015 10:07:16 +0000 Subject: [PATCH 063/105] Support multiple domains --- conf/settings.json | 2 +- script/build.js | 34 +++++++++++++++++++++------------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/conf/settings.json b/conf/settings.json index f439494..d5862d8 100644 --- a/conf/settings.json +++ b/conf/settings.json @@ -41,7 +41,7 @@ "contentScriptFiles": [ "src/extension.js" ], "contentStyleFiles": [ "src/extension.css" ], - "match_domain": "babelext.com", // or *.babelext.com to include subdomains + "match_domains": [ "babelext.com" ], // or *.babelext.com to include subdomains // whether to match https://: "match_secure_domain": true, diff --git a/script/build.js b/script/build.js index 9d7f952..f2dda95 100755 --- a/script/build.js +++ b/script/build.js @@ -434,10 +434,12 @@ function build_safari() { var match_domains = get_node('Allowed Domains'); while (match_domains.firstChild) match_domains.removeChild(match_domains.firstChild); - var domain = document.createElement("string"); - domain.textContent = settings.match_domain; - match_domains.appendChild( document.createTextNode('\n\t\t\t\t') ); - match_domains.appendChild(domain); + settings.match_domains.forEach(function(match_domain) { + var domain = document.createElement("string"); + domain.textContent = match_domain; + match_domains.appendChild( document.createTextNode('\n\t\t\t\t') ); + match_domains.appendChild(domain); + }); match_domains.appendChild( document.createTextNode('\n\t\t\t') ); var match_secure_domain = get_node('Include Secure Pages'); @@ -546,10 +548,15 @@ function build_firefox() { // Create settings.js: fs.write( 'build/Firefox/lib/settings.js', - ( settings.match_secure_domain - ? 'exports.include = ["http://' + settings.match_domain + '/*","https://' + settings.match_domain + '/*"];\n' - : 'exports.include = ["http://' + settings.match_domain + '/*"];\n' - ) + + 'exports.include = [' + + settings.match_domains.map(function(domain) { + return ( + settings.match_secure_domain + ? '"http://' + domain + '/*","https://' + domain + '/*"' + : '"http://' + domain + '/*"' + ) + }).join(',') + + '];\n' + 'exports.contentScriptWhen = "' + when_string[settings.contentScriptWhen] + '";\n' + 'exports.contentScriptFile = ' + JSON.stringify(settings.contentScriptFiles) + ";\n" + 'exports.contentStyleFile = ' + JSON.stringify(settings.contentStyleFiles || []) + ";\n" @@ -655,7 +662,9 @@ function build_chrome() { 'late' : 'document_idle' }; - var match_url = ( settings.match_secure_domain ? "*://" : "http://" ) + settings.match_domain + '/*'; + var match_urls = settings.match_domains.map(function(domain) { + return ( settings.match_secure_domain ? "*://" : "http://" ) + domain + '/*'; + }); var manifest = { "name": settings.title, @@ -668,19 +677,18 @@ function build_chrome() { }, "content_scripts": [ { - "matches": [ match_url ], + "matches": match_urls, "js": settings.contentScriptFiles, "run_at": when_string[settings.contentScriptWhen] } ], "icons": settings.icons, - "permissions": [ - match_url, + "permissions": match_urls.concat([ "contextMenus", "tabs", "history", "notifications" - ] + ]) }; var contentFiles = settings.contentScriptFiles.concat( From 5ac05608c9717636e7b2ca2c1701ddaf84f1a3f9 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 15 Feb 2015 23:50:29 +0000 Subject: [PATCH 064/105] Remove trailing whitespace throughout the code --- build/Chrome/background.js | 4 +- build/Chrome/chrome-bootstrap.css | 8 +-- build/Chrome/manifest.json | 13 ++-- build/Firefox/lib/settings.js | 3 +- build/Firefox/package.json | 74 +++++++++++++++++++- build/Safari.safariextension/Info.plist | 4 +- build/Safari.safariextension/background.html | 14 ++-- sink.html | 4 +- 8 files changed, 101 insertions(+), 23 deletions(-) diff --git a/build/Chrome/background.js b/build/Chrome/background.js index 27b6227..26e448a 100644 --- a/build/Chrome/background.js +++ b/build/Chrome/background.js @@ -16,7 +16,7 @@ chrome.runtime.onMessage.addListener( if (request.method === "POST") { xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); // xhr.setRequestHeader("Content-length", request.data.length); - // xhr.setRequestHeader("Connection", "close"); + // xhr.setRequestHeader("Connection", "close"); } xhr.onreadystatechange = function() { if (xhr.readyState === 4) { @@ -31,7 +31,7 @@ chrome.runtime.onMessage.addListener( case 'createTab': var newIndex, focus = (request.background !== true); - + if (typeof(request.index) !== 'undefined') { newIndex = request.index; } else { diff --git a/build/Chrome/chrome-bootstrap.css b/build/Chrome/chrome-bootstrap.css index da89e9d..c481188 100644 --- a/build/Chrome/chrome-bootstrap.css +++ b/build/Chrome/chrome-bootstrap.css @@ -1,18 +1,18 @@ /* * The MIT License - * + * * Copyright (c) 2012 Chrome Bootstrap authors - * + * * 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 diff --git a/build/Chrome/manifest.json b/build/Chrome/manifest.json index d4d6f7e..0bba4cd 100644 --- a/build/Chrome/manifest.json +++ b/build/Chrome/manifest.json @@ -6,6 +6,7 @@ "description": "An extension created with BabelExt - www.babelext.com", "background": { "scripts": [ + "preferences.js", "background.js" ] }, @@ -15,10 +16,13 @@ "*://babelext.com/*" ], "js": [ - "BabelExt.js", - "extension.js" + "lib/BabelExt.js", + "src/extension.js" ], - "run_at": "document_end" + "run_at": "document_end", + "css": [ + "src/extension.css" + ] } ], "icons": {}, @@ -29,5 +33,6 @@ "history", "notifications", "storage" - ] + ], + "options_page": "options.html" } diff --git a/build/Firefox/lib/settings.js b/build/Firefox/lib/settings.js index c88ed14..ab3075f 100644 --- a/build/Firefox/lib/settings.js +++ b/build/Firefox/lib/settings.js @@ -1,3 +1,4 @@ exports.include = ["http://babelext.com/*","https://babelext.com/*"]; exports.contentScriptWhen = "ready"; -exports.contentScriptFile = ["BabelExt.js","extension.js"]; +exports.contentScriptFile = ["lib/BabelExt.js","src/extension.js"]; +exports.contentStyleFile = ["src/extension.css"]; diff --git a/build/Firefox/package.json b/build/Firefox/package.json index 7ada1fd..94eee61 100644 --- a/build/Firefox/package.json +++ b/build/Firefox/package.json @@ -5,5 +5,77 @@ "version": "0.95", "title": "BabelExt", "id": "abcdef01-2345-6789-9876-543210fedcba", - "name": "babelext_your_name_here" + "name": "babelext_your_name_here", + "preferences": [ + { + "name": "myBool", + "type": "bool", + "title": "myBool preference title", + "description": "myBool short description for the preference", + "value": false + }, + { + "name": "myBoolint", + "type": "boolint", + "title": "myBoolint preference title", + "description": "myBoolint short description for the preference", + "off": "1", + "on": "2", + "value": 1 + }, + { + "name": "myInteger", + "type": "integer", + "title": "myInteger preference title", + "description": "myInteger short description for the preference", + "value": 2 + }, + { + "name": "myString", + "type": "string", + "title": "myString preference title", + "description": "myString short description for the preference", + "value": "this is the default string value" + }, + { + "name": "myMenulist", + "type": "menulist", + "title": "myMenulist preference title", + "value": 0, + "options": [ + { + "value": "0", + "label": "first label" + }, + { + "value": "1", + "label": "second label" + }, + { + "value": "2", + "label": "third label" + } + ] + }, + { + "name": "myRadio", + "type": "radio", + "title": "myRadioTitle", + "value": "a", + "options": [ + { + "value": "a", + "label": "first label" + }, + { + "value": "b", + "label": "second label" + }, + { + "value": "c", + "label": "third label" + } + ] + } + ] } diff --git a/build/Safari.safariextension/Info.plist b/build/Safari.safariextension/Info.plist index 23f333b..f06d85e 100644 --- a/build/Safari.safariextension/Info.plist +++ b/build/Safari.safariextension/Info.plist @@ -57,11 +57,11 @@ End - extension.js + src/extension.js Start - BabelExt.js + lib/BabelExt.js Stylesheets diff --git a/build/Safari.safariextension/background.html b/build/Safari.safariextension/background.html index 763285c..be36da3 100644 --- a/build/Safari.safariextension/background.html +++ b/build/Safari.safariextension/background.html @@ -1,6 +1,6 @@ - - \ No newline at end of file + diff --git a/sink.html b/sink.html index 7e462b1..a5d49dc 100644 --- a/sink.html +++ b/sink.html @@ -33,7 +33,7 @@ - +

BabelExt Kitchen Sink Demos

@@ -177,4 +177,4 @@

After installing the extension, refresh this page to see the features that B

- \ No newline at end of file + From c8cfac577fe2eb66917608c9e1d373dba6e8d5f3 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 16 Feb 2015 07:45:20 +0000 Subject: [PATCH 065/105] Add page.openBinary() in build.js --- script/build.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/script/build.js b/script/build.js index f2dda95..0ab1de7 100755 --- a/script/build.js +++ b/script/build.js @@ -267,6 +267,23 @@ function page( url, callback ) { page.settings.loadImages = false; + page.openBinary = function(url, settings, callback) { + + // PhantomJS refuses to download chunked data, do it with `curl` instead (TODO: make this work in Windows): + + if ( !callback ) { + callback = settings; + settings = {}; + } + + var args = [ "--silent", url, '-L' ]; + + if ( settings.data ) args = args.concat([ '-d', settings.data ]); + if ( settings.out_file ) args = args.concat([ '-o', settings.out_file ]); + + childProcess.execFile( 'curl', args, null, callback ); + } + return page.open( url, function(status) { if (status == 'success') { callback(page); @@ -594,11 +611,7 @@ function build_firefox() { build_xpi(); } else { console.log( 'Downloading Firefox Addon SDK...' ); - // PhantomJS refuses to download any file as large as the SDK (I think it's either about the encoding or the file size) - // do it with `curl` instead: - console.log( 'Unpacking Firefox Addon SDK...', status ); - childProcess.execFile( 'curl', ['--silent',response.redirectURL,'-o','temporary_file.tar.gz'], null, function(err, stdout, stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } + page.openBinary( response.redirectURL, { out_file: 'temporary_file.tar.gz' }, function() { fs.makeDirectory('build/firefox-addon-sdk'); childProcess.execFile( 'tar', ["zxf",'temporary_file.tar.gz','-C','build/firefox-addon-sdk','--strip-components=1'], null, function(err,stdout,stderr) { if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } @@ -1085,12 +1098,7 @@ function release_chrome(login_info) { function get_auth_key(code) { var post_data = "grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id=" + login_info.client_id + "&client_secret=" + login_info.client_secret + "&code=" + code; - - // PhantomJS refuses to download chunked data, do it with `curl` instead: - childProcess.execFile( 'curl', ["--silent","https://accounts.google.com/o/oauth2/token",'-d',post_data], null, function(err, json, stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } - upload_and_publish( JSON.parse(json) ); - }); + page.openBinary( 'https://accounts.google.com/o/oauth2/token', { data: post_data }, upload_and_publish ); } function upload_and_publish(data) { From c5bd204baa3b47fad1b9b6f7cee8385abd559b6d Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 16 Feb 2015 07:46:09 +0000 Subject: [PATCH 066/105] Replace PhantomJS' execFile() with something more useful --- script/build.js | 64 +++++++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/script/build.js b/script/build.js index 0ab1de7..5caebbc 100755 --- a/script/build.js +++ b/script/build.js @@ -38,6 +38,39 @@ phantom.onError = function(msg, trace) { phantom.exit(1); }; +/* + * Replace PhantomJS' execFile() with something more useful: + */ +var execFile = childProcess.execFile; +childProcess.execFile = function(cmd, args, opts, cb) { + + // need to check both the callback and value of "exit": + var calls = 0, err, stdout, stderr, code; + + // run the command and get stdout/stderr: + var ctx = execFile.call( childProcess, cmd, args, opts, function(_err,_stdout,_stderr) { + err = _err; + stdout = _stdout; + stderr = _stderr; + if ( calls++ ) run_callback(); + }); + + // also get the exit code: + ctx.on("exit", function (_code) { + code = _code; + if ( calls++ ) run_callback(); + }); + + // once we've got all the information, print STDERR and continue if there was no error: + function run_callback() { + if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); + if ( code ) program_counter.end(code); + else if ( cb ) cb(null, stdout, stderr, code); + } + + return ctx; +} + /* * Create a symbolic link from source to target */ @@ -45,13 +78,9 @@ function symbolicLink( source, target ) { target.replace(/\//g, function() { source = '../' + source }); if ( ! fs.isLink(target) ) { if ( system.os.name == 'windows' ) { - childProcess.execFile('mklink', [target,source], function(err, stdout, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); - }); + childProcess.execFile('mklink', [target,source] ); } else { - childProcess.execFile('ln', ["-s",source,target], null, function(err, stdout, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); - }); + childProcess.execFile('ln', ["-s",source,target] ); } } } @@ -62,13 +91,9 @@ function symbolicLink( source, target ) { function hardLink( source, target ) { if ( fs.exists(target) ) fs.remove(target); if ( system.os.name == 'windows' ) { - childProcess.execFile('mklink', ['/H',target,source], function(err, stdout, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); - }); + childProcess.execFile('mklink', ['/H',target,source]); } else { - childProcess.execFile('ln', [source,target], null, function(err, stdout, stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); - }); + childProcess.execFile('ln' , [source,target]); } } @@ -280,6 +305,7 @@ function page( url, callback ) { if ( settings.data ) args = args.concat([ '-d', settings.data ]); if ( settings.out_file ) args = args.concat([ '-o', settings.out_file ]); + if ( settings.cookies ) args = args.concat([ '-c', settings.cookies ]); childProcess.execFile( 'curl', args, null, callback ); } @@ -403,8 +429,6 @@ function get_changelog(callback) { // call the callback with the changelog text local_settings.changelog_command.splice(1), null, function(err,changelog,stderr) { - if ( stderr != '' ) console.log(stderr.replace(/\n$/,'')); - if (err) throw err; if ( changelog == '' ) { console.log( "Error: empty changelog" ); return program_counter.end(1); @@ -614,7 +638,6 @@ function build_firefox() { page.openBinary( response.redirectURL, { out_file: 'temporary_file.tar.gz' }, function() { fs.makeDirectory('build/firefox-addon-sdk'); childProcess.execFile( 'tar', ["zxf",'temporary_file.tar.gz','-C','build/firefox-addon-sdk','--strip-components=1'], null, function(err,stdout,stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } fs.remove('temporary_file.tar.gz'); fs.write( 'build/firefox-addon-sdk-url.txt', response.redirectURL, 'w' ); build_xpi(); @@ -638,7 +661,6 @@ function build_firefox() { // Move the .xpi into place, fix its install.rdf, and update firefox-unpacked: function finalise_xpi(err, stdout, stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } fs.makeDirectory('out'); var xpi = 'out/' + settings.name + '.xpi'; if ( fs.exists(xpi) ) fs.remove(xpi); @@ -646,7 +668,6 @@ function build_firefox() { fs.removeTree('build/firefox-unpacked'); fs.makeDirectory('build/firefox-unpacked'); childProcess.execFile( 'unzip', ['-d','build/firefox-unpacked',xpi], null, function(err,stdout,stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } fs.write( 'build/firefox-unpacked/install.rdf', fs.read('build/firefox-unpacked/install.rdf').replace( /.*<\/em:maxVersion>/, '' + settings.firefox_max_version + '' ) @@ -805,16 +826,12 @@ function build_chrome() { if (fs.exists('build/Chrome.pem')) { build_crx(); } else { - childProcess.execFile(chrome_command, ["--pack-extension=build/Chrome"], null, function (err, stdout, stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } - build_crx(); - }); + childProcess.execFile(chrome_command, ["--pack-extension=build/Chrome"], null, build_crx ); }; // Build the .crx, move it into place, and build the upload zip file: function build_crx() { childProcess.execFile(chrome_command, ["--pack-extension=build/Chrome","--pack-extension-key=build/Chrome.pem"], null, function (err, stdout, stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } if ( stdout != 'Created the extension:\n\nbuild/Chrome.crx\n' ) console.log(stdout.replace(/\n$/,'')); var crx = 'out/' + settings.name + '.crx'; if ( fs.exists(crx) ) fs.remove(crx); @@ -829,7 +846,6 @@ function build_chrome() { , null, function(err,stdout,stderr) { - if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } console.log('Built out/chrome-store-upload.zip'); return program_counter.end(0); } @@ -1103,6 +1119,8 @@ function release_chrome(login_info) { function upload_and_publish(data) { + data = JSON.parse(data); + var page = webPage.create(); page.customHeaders = { "Authorization": "Bearer " + data.access_token, From ae76bdddb1819db325ce68242fa8888b87c0aa88 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 16 Feb 2015 11:50:38 +0000 Subject: [PATCH 067/105] Neater implementation of waitForElementsPresent() --- script/build.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/script/build.js b/script/build.js index 5caebbc..c2de86e 100755 --- a/script/build.js +++ b/script/build.js @@ -143,11 +143,9 @@ function _waitForElementsPresent( selectors, callback ) { // call callback when return this.waitForEvent( function() { - var missing_elements = []; - selectors.forEach( + var missing_elements = selectors.filter( function(selector) { - if ( ! page.evaluate(function(selector) {return document.querySelector(selector)}, selector ) ) - missing_elements.push(selector); + return ! page.evaluate(function(selector) { return document.querySelector(selector) }, selector ); } ); if ( missing_elements.length ) From 818fe40b654bb304c7c73f2df8313156572e9f42 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 16 Feb 2015 11:54:30 +0000 Subject: [PATCH 068/105] Increase timeout in build.js It takes many seconds to generate Safari certificates --- script/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/build.js b/script/build.js index c2de86e..2c249e1 100755 --- a/script/build.js +++ b/script/build.js @@ -105,7 +105,7 @@ function _waitForEvent( test, callback ) { // low-level interface - see waitFor* // originally based on http://newspaint.wordpress.com/2013/04/05/waiting-for-page-to-load-in-phantomjs/ - var timeout = 10000; + var timeout = 20000; var expiry = new Date().getTime() + timeout; var interval = setInterval(checkEvent,100); From d4e4955b8c98d571d95ac0c5e37690d5751cb1f7 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 16 Feb 2015 11:54:52 +0000 Subject: [PATCH 069/105] Build signed Safari packages --- .gitignore | 3 + README.md | 35 +++- build/Safari.safariextension/.gitignore | 1 - build/Safari.safariextension/Info.plist | 6 +- conf/local_settings.json.example | 6 + conf/settings.json | 4 +- script/build.js | 232 +++++++++++++++++++++--- 7 files changed, 253 insertions(+), 34 deletions(-) delete mode 100644 build/Safari.safariextension/.gitignore diff --git a/.gitignore b/.gitignore index 70a618b..4ada02d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ /build/firefox-unpacked /build/firefox-addon-sdk-url.txt /build/firefox-addon-sdk +/build/safari-certs/ +/build/xar-url.txt +/build/xar/ /conf/local_settings.json /build/Chrome.pem /out diff --git a/README.md b/README.md index 3bdf0e3..059f0e3 100644 --- a/README.md +++ b/README.md @@ -65,18 +65,15 @@ First, download all of the source from Github and put it together within a folde Then, download PhantomJS (http://phantomjs.org), which is used to build and deploy extensions. -In UNIX-based OSes, run `./script/build.sh build` to build packages, and `./script/build.sh release` -to release them. +In UNIX-based OSes, run `./script/build.sh build ` to build packages for each browser, +and `./script/build.sh release ` to release them to the various extension sites. The build system hasn't been tested under Windows yet - your best bet is probably to look at the scripts and write a Windows equivalent. If it's any good, please send in a patch! -**IMPORTANT SAFARI NOTE:** Safari has a "security feature" that is not documented, gives no user -feedback at all, and can be a HUGE time sink if you don't know about it! If you have any -files in your extension folder that are symlinks, Safari will **silently** ignore them. -With Safari, a hard link will work, but a symbolic link will not. If you made the links -yourself instead of using the batch file, and your extension is doing nothing at all in -Safari, double check that! +The build system maintains browser-specific `build` directories based on `conf/settings.json`. +It uses symbolic links where possible, but falls back to hard links for Chrome and Safari +(which silently ignore symlinks). One last Safari quirk: if the directory does not end in ".safariextension", it will not be recognized by Safari. Don't remove that from the name! @@ -130,6 +127,28 @@ recognized by Safari. Don't remove that from the name! - Further Safari development information can be found at [https://developer.apple.com/library/safari/#documentation/Tools/Conceptual/SafariExtensionGuide/Introduction/Introduction.html](https://developer.apple.com/library/safari/#documentation/Tools/Conceptual/SafariExtensionGuide/Introduction/Introduction.html) +#### Certificates #### + +Safari requires all packages to be signed with a private key that's been registered with Apple. +You can develop unpacked extensions without a license, but you will need a (free) Apple Developer +account to build a package. You will also need to create a private key, which you can do with: + + openssl req -new -nodes -newkey rsa:2048 -keyout build/safari-info/id.rsa -out apple-cert.csr + +Apple seems to prefer you have a single private key per Apple Developer account. +If you maintain several projects with one account, consider linking build/safari-certs to a central location. + +BabelExt will automatically register your key and download extra certificates if you pass in your +username and password. Here are the steps if you prefer to do it by hand: + +- Go through [Apple's Certificate Request process](https://developer.apple.com/account/safari/certificate/certificateRequest.action) and save your certificate as `build/safari-certs/local.cer` +- Download [Apple's Worldwide Developer Relations Certificate](https://developer.apple.com/certificationauthority/AppleWWDRCA.) to `build/safari-certs/AppleIncRootCertificate.cer` +- Download [Apple's Root Certificate](https://www.apple.com/appleca/AppleIncRootCertificate.cer) to `build/safari-certs/AppleWWDRCA.cer` +- Download and compile [a modified version of the "xar" tool](http://mackyle.github.io/xar/) as `build/xar` + +Note: some online documentation refers to these keys as `cert00`, `cert01` and `cert02` +(these are the names `xar` uses when extracting them from a package) + ## Resetting extension data ## If your extension uses storage or preferences, you will need to test the extension data with diff --git a/build/Safari.safariextension/.gitignore b/build/Safari.safariextension/.gitignore deleted file mode 100644 index dcaffc0..0000000 --- a/build/Safari.safariextension/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/*.js diff --git a/build/Safari.safariextension/Info.plist b/build/Safari.safariextension/Info.plist index f06d85e..594e6b9 100644 --- a/build/Safari.safariextension/Info.plist +++ b/build/Safari.safariextension/Info.plist @@ -5,11 +5,11 @@ Author honestbleeps Builder Version - 8536.30.1 + 8537.85.12.18 CFBundleDisplayName BabelExt CFBundleIdentifier - com.honestbleeps.abcdef01-2345-6789-9876-543210fedcba + com.honestbleeps.abcdefghijklmnopqabcdefghijklmnopqabcdefghij CFBundleInfoDictionaryVersion 6.0 CFBundleShortVersionString @@ -71,6 +71,8 @@
Description An extension created with BabelExt - www.babelext.com + DeveloperIdentifier + ABCDEFGHIJ ExtensionInfoDictionaryVersion 1.0 Permissions diff --git a/conf/local_settings.json.example b/conf/local_settings.json.example index 23f2e08..5a36a51 100644 --- a/conf/local_settings.json.example +++ b/conf/local_settings.json.example @@ -44,6 +44,12 @@ tested_on: 'Opera 25 Developer Linux' // List of versions you've tested on (see opera://about for your version) }, + // set this to build a signed Safari extension: + safari_login_info: { + username: 'user@example.org', + password: 'password123' // optional - defaults to the environment variable 'OPERA_PASSWORD' + }, + // Command to get the changelog for your latest version. // At the time of writing, this was required by Opera and hadn't yet been implemented for other browsers: changelog_command: [ 'git', 'log', '--pretty=* %s', '@{u}..HEAD', '--reverse', '--first-parent' ], diff --git a/conf/settings.json b/conf/settings.json index d5862d8..5352600 100644 --- a/conf/settings.json +++ b/conf/settings.json @@ -125,6 +125,8 @@ }, ], - "firefox_max_version": '32.*' + "firefox_max_version": '32.*', + + "safari_team_id": 'ABCDEFGHIJ' // visible at https://developer.apple.com/membercenter/index.action#accountSummary } diff --git a/script/build.js b/script/build.js index 2c249e1..ab617cd 100755 --- a/script/build.js +++ b/script/build.js @@ -3,7 +3,7 @@ /* * Phantom JS build script * - * Usage: phantomjs + * Usage: phantomjs * * * PhantomJS is a headless web browser, which allows us to automate @@ -303,7 +303,7 @@ function page( url, callback ) { if ( settings.data ) args = args.concat([ '-d', settings.data ]); if ( settings.out_file ) args = args.concat([ '-o', settings.out_file ]); - if ( settings.cookies ) args = args.concat([ '-c', settings.cookies ]); + if ( settings.cookies ) args = args.concat([ '-H', 'Cookie: ' + settings.cookies ]); childProcess.execFile( 'curl', args, null, callback ); } @@ -441,7 +441,7 @@ function get_changelog(callback) { // call the callback with the changelog text * BUILD COMMANDS */ -function build_safari() { +function build_safari(login_info) { var when_string = { 'early' : 'Start', @@ -462,14 +462,40 @@ function build_safari() { get_node(key).textContent = value; } + /* + * PART ONE: build the Safari.safariextension directory: + */ + + // BabelExt IDs are UUIDs, but Safari IDs must be alphabetical: + var map = { + '0': 'a', + '1': 'b', + '2': 'c', + '3': 'd', + '4': 'e', + '5': 'f', + '6': 'g', + '7': 'h', + '8': 'i', + '9': 'j', + 'a': 'k', + 'b': 'l', + 'g': 'm', + 'd': 'n', + 'e': 'o', + 'f': 'p', + '-': 'q' + }; + get_node('Author').textContent = settings.author; get_node('CFBundleDisplayName' ).textContent = settings.title; - get_node('CFBundleIdentifier' ).textContent = 'com.honestbleeps.' + settings.id; + get_node('CFBundleIdentifier' ).textContent = 'com.honestbleeps.' + settings.id.replace( /(.)/g, function(char) { return map[char] }); get_node('CFBundleShortVersionString').textContent = settings.version; get_node('CFBundleVersion' ).textContent = settings.version; get_node('Description' ).textContent = settings.description; get_node('Website' ).textContent = settings.website; + get_node('DeveloperIdentifier' ).textContent = settings.safari_team_id || '(not set)'; var match_domains = get_node('Allowed Domains'); while (match_domains.firstChild) match_domains.removeChild(match_domains.firstChild); @@ -563,6 +589,171 @@ function build_safari() { 'w' ); + + /* + * PART TWO: build a signed .safariextz file + */ + + program_counter.begin(); + + if ( fs.exists('build/safari-certs/AppleWWDRCA.cer') ) { + check_xar(); + } else { + + if ( !login_info || login_info.skip ) { + console.log( 'Please add Safari login details to local_settings.json to build a Safari package' ); + return program_counter.end(0); + } + + if ( !login_info.password ) { + if ( system.env.hasOwnProperty('APPLE_PASSWORD') ) { + login_info.password = system.env.APPLE_PASSWORD; + } else { + console.log("Please specify a password for apple.com"); + return program_counter.end(1); + } + } + + if ( !fs.exists('build/safari-certs/id.rsa') ) { + console.log( + "Please generate a private key and Certificater Signature Request.\n" + + "The private key should not have an associated password.\n" + + "Example command:\n" + + "openssl req -new -nodes -newkey rsa:2048 -keyout build/safari-certs/id.rsa -out build/safari-certs/request.csr" + ); + return program_counter.end(1); + } + + console.log( 'Generating keys...' ); + page( 'https://developer.apple.com/account/safari/certificate/certificateRequest.action', function(page) { + + var onError = page.onError; + page.onError = function(msg, trace) { + // Ignore expected error + if ( msg != "TypeError: 'undefined' is not an object (evaluating 'document.form1.submit')" ) { + onError.call( page, msg, trace ); + } + }; + + page.submit_form( + '#submitButton2', + { + '#accountname' : login_info.username, + '#accountpassword': login_info.password + }, + function() { + page.onError = onError; + page.waitForElementsPresent( + [ 'form[name="certificateRequest"]' ], + function() { + page.click('a.submit'); + page.waitForElementsPresent( + [ '#certificateSubmit' ], + function() { + + page.submit_form( + 'a.submit', + { + 'input[name="upload"]': 'build/safari-certs/request.csr', + }, + function() { + page.waitForElementsPresent( + [ '.downloadForm' ], + function() { + var download_url = page.evaluate(function() { + return document.getElementsByClassName('blue')[0].getAttribute('href') + }); + var cookies = page.cookies.map(function(cookie) { return cookie.name + '=' + cookie.value }); + page.openBinary( 'https://developer.apple.com' + download_url, { cookies: cookies.join('; '), out_file: 'build/safari-certs/local.cer' }, function() { + page.openBinary( 'https://www.apple.com/appleca/AppleIncRootCertificate.cer', { out_file: 'build/safari-certs/AppleIncRootCertificate.cer' }, function() { + page.openBinary('https://developer.apple.com/certificationauthority/AppleWWDRCA.cer', { out_file: 'build/safari-certs/AppleWWDRCA.cer' }, check_xar ); + }); + }); + }); + }); + } + ); + } + ); + } + ); + + }); + + } + + function check_xar() { + page( 'http://mackyle.github.io/xar/', function(page) { + + var xar_url = page.evaluate(function() { + return document.getElementsByClassName('down')[0].parentNode.getAttribute('href') + }); + + if ( fs.exists('build/xar-url.txt') && fs.read('build/xar-url.txt') == xar_url ) { + console.log( 'XAR is up-to-date.' ); + build_safariextz(); + } else { + console.log( 'Downloading xar archiver...' ); + page.openBinary(xar_url, { out_file: 'temporary_file.tar.gz' }, function() { + console.log( 'Unpacking xar archiver...', status ); + if ( fs.exists( 'build/xar' ) ) fs.removeTree('build/xar'); + fs.makeDirectory('build/xar'); + childProcess.execFile( 'tar', ["zxf",'temporary_file.tar.gz','-C','build/xar','--strip-components=1'], null, function(err,stdout,stderr) { + console.log( 'Building xar archiver...', status ); + if ( system.os.name == 'windows' ) { + // TODO: fill in real Windows values here (the following line is just a guess): + childProcess.execFile( 'cmd' , [ 'cd build\\xar ; ./configure ; make'], null, finalise_xar ); + } else { + childProcess.execFile( 'bash', ['-c','cd build/xar && ./configure && make'], null, finalise_xar ); + } + + function finalise_xar(err,stdout,stderr) { + fs.remove('temporary_file.tar.gz'); + fs.write( 'build/xar-url.txt', xar_url, 'w' ); + build_safariextz(); + } + + }); + }); + } + + }); + } + + function build_safariextz() { + + function run_commands(commands, then) { + function run_command(err, stdout, stderr) { + if ( commands.length ) { + var command = commands.shift(); + return childProcess.execFile( command[0], command.splice(1), null, run_command ); + } else { + return then(err, stdout, stderr) + } + } + run_command(); + } + + fs.changeWorkingDirectory('build'); + + var xar = './xar/src/xar'; + var safariextz = '../out/' + settings.name + '.safariextz'; + + run_commands([ + [ xar, '-czf', safariextz, '--distribution', 'Safari.safariextension' ], + [ xar, '-f', safariextz, '--sign', '--digestinfo-to-sign', 'safari-certs/tmp.dat', '--sig-size', 256, '--cert-loc', 'safari-certs/local.cer', '--cert-loc', 'safari-certs/AppleWWDRCA.cer', '--cert-loc', 'safari-certs/AppleIncRootCertificate.cer' ], + [ 'openssl', 'rsautl', '-sign', '-inkey', 'safari-certs/id.rsa', '-in', 'safari-certs/tmp.dat', '-out', 'safari-certs/tmp.sig' ], + [ xar, '-f', safariextz, '--inject-sig', 'safari-certs/tmp.sig' ] + ], function() { + fs.remove('safari-certs/tmp.dat'); + fs.remove('safari-certs/tmp.sig'); + fs.changeWorkingDirectory('..'); + console.log('Built ' + safariextz.substr(3)); + return program_counter.end(0); + }); + } + + } function build_firefox() { @@ -676,7 +867,7 @@ function build_firefox() { }); fs.changeWorkingDirectory('build/firefox-unpacked'); childProcess.execFile( 'zip', ['../'+xpi,'install.rdf'], null, function(err,stdout,stderr) { - fs.changeWorkingDirectory('..'); + fs.changeWorkingDirectory('../..'); if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } console.log('Built ' + xpi + '\n\033[1mRemember to restart Firefox if you added/removed any files!\033[0m'); return program_counter.end(0); @@ -1223,15 +1414,10 @@ function release_opera(login_info) { } function release_safari() { - /* - * Safari support is limited at the moment, as it's the least-used browser and the hardest to support. - * - * To release a Safari extension, start here: https://developer.apple.com/programs/safari/ - * For instructions on building a Safari extension package on the command line, start here: http://developer.streak.com/2013/01/how-to-build-safari-extension-using.html - * - * Patches welcome! - * - */ + console.log( + 'The Safari extensions gallery just links to your actual download site.\n' + + 'This function is included only for completeness' + ); } /* @@ -1242,26 +1428,28 @@ var args = system.args; function usage() { console.log( - 'Usage: ' + args[0] + ' []\n' + + 'Usage: ' + args[0] + ' \n' + 'Commands:\n' + - ' build - builds extensions for all browsers\n' + - ' release - release extension to either "amo" (addons.mozilla.org) "chrome" (Chrome store) or "opera" (Opera site)' + ' build - builds extensions for "amo" (Firefox) "chrome" or "safari"\n' + + ' release - release extension to "amo" (addons.mozilla.org) "chrome" (Chrome store), "opera" (opera site) or "safari" (extensions gallery)' ); phantom.exit(1); } program_counter.begin(); +if ( args.length != 3 ) usage(); + switch ( args[1] || '' ) { case 'build': - if ( args.length != 2 ) usage(); - build_safari(); - build_firefox(); - build_chrome (); + switch ( args[2] ) { + case 'firefox': build_firefox(local_settings. amo_login_info); break; + case 'chrome' : build_chrome (local_settings.chrome_login_info); break; + case 'safari' : build_safari (local_settings.safari_login_info); break; + } break; case 'release': - if ( args.length != 3 ) usage(); switch ( args[2] ) { case 'amo' : release_amo (local_settings. amo_login_info); break; case 'chrome': release_chrome(local_settings.chrome_login_info); break; From 622f4217a45c017482e21590235f46980df3aca8 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 16 Feb 2015 12:07:50 +0000 Subject: [PATCH 070/105] Tidied up .gitignore --- .gitignore | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 4ada02d..ab4c3fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,23 @@ +# Files and directories used during the build process: /build/firefox-unpacked /build/firefox-addon-sdk-url.txt /build/firefox-addon-sdk -/build/safari-certs/ /build/xar-url.txt /build/xar/ -/conf/local_settings.json -/build/Chrome.pem -/out -*~ + +# Files and directories containing unpacked extensions: /build/Chrome /build/Firefox/*.png /build/Firefox/data /build/Safari.safariextension + +# output directory: +/out + +# Files containing sensitive information that must never go in version control: +/build/safari-certs +/build/Chrome.pem +/conf/local_settings.json + +# miscellaneous: +*~ From fce824f5105ea8e468c414c06622a9d24404341f Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Tue, 17 Feb 2015 04:41:34 +0000 Subject: [PATCH 071/105] Ignore /build/Firefox/icons --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ab4c3fd..f18f7c3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /build/Chrome /build/Firefox/*.png /build/Firefox/data +/build/Firefox/icons /build/Safari.safariextension # output directory: From 0492cdaeab00465a7698739e1de0d29311f4ae17 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Tue, 17 Feb 2015 04:44:16 +0000 Subject: [PATCH 072/105] Minor improvements to README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 059f0e3..e81ebca 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ thought to how to make it more universally useful. First, download all of the source from Github and put it together within a folder. -Then, download PhantomJS (http://phantomjs.org), which is used to build and deploy extensions. +Then, download [PhantomJS](http://phantomjs.org), which is used to build and deploy extensions. In UNIX-based OSes, run `./script/build.sh build ` to build packages for each browser, and `./script/build.sh release ` to release them to the various extension sites. @@ -152,7 +152,7 @@ Note: some online documentation refers to these keys as `cert00`, `cert01` and ` ## Resetting extension data ## If your extension uses storage or preferences, you will need to test the extension data with -different stored values. Apart from Safari, all the browsers let you creat add multiple +different stored values. Apart from Safari, all the browsers let you create multiple profiles ("users" in Chrome), so you might want to create throwaway profiles for use during testing. @@ -165,5 +165,5 @@ extension data by deleting all files matching /Local*/*` +to release and update metadata. From 578c850cca8c56bcb9e5d863ebf75cd8fcb410ed Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Tue, 17 Feb 2015 04:49:07 +0000 Subject: [PATCH 073/105] Add BabelExt.resources --- .gitignore | 1 + README.md | 5 +- conf/settings.json | 3 + lib/BabelExt.js | 25 ++++++ res/extension.html | 9 ++ script/build.js | 219 +++++++++++++++++++++++++++++++++------------ src/extension.js | 35 ++++++++ 7 files changed, 239 insertions(+), 58 deletions(-) create mode 100644 res/extension.html diff --git a/.gitignore b/.gitignore index f18f7c3..ea19771 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /build/firefox-addon-sdk /build/xar-url.txt /build/xar/ +/lib/BabelExtResources.js # Files and directories containing unpacked extensions: /build/Chrome diff --git a/README.md b/README.md index e81ebca..0a0c7df 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ has its own function calls and way of working, including, but not limited to: - Accessing and controlling tabs (i.e. opening a link in a new one and choosing if it's focused) - Cross domain http requests (extensions require) - Storing data (using HTML5 localStorage or similar/equivalent engines) +- Managing resources (like large HTML snippets that are hard to read in raw JavaScript) - Managing add-on preferences (which some browsers call options or settings) - Triggering notifications (desktop or browser, depending on the browser's particular level of support) - Adding URLs to history (to mark links as visited) @@ -75,8 +76,8 @@ The build system maintains browser-specific `build` directories based on `conf/s It uses symbolic links where possible, but falls back to hard links for Chrome and Safari (which silently ignore symlinks). -One last Safari quirk: if the directory does not end in ".safariextension", it will not be -recognized by Safari. Don't remove that from the name! +It is recommended run `./script/build.sh maintain &` in the background. +This automatically fixes broken hard links and updates `BabelExt.resources` every few seconds. ## Instructions for loading/testing an extension in each browser ## diff --git a/conf/settings.json b/conf/settings.json index 5352600..11ebea3 100644 --- a/conf/settings.json +++ b/conf/settings.json @@ -41,10 +41,13 @@ "contentScriptFiles": [ "src/extension.js" ], "contentStyleFiles": [ "src/extension.css" ], + "resources": [ "res/extension.html" ], "match_domains": [ "babelext.com" ], // or *.babelext.com to include subdomains // whether to match https://: "match_secure_domain": true, + "maintenanceInterval": 5 * 1000, // the 'maintain' command will pause for this long between updates + "environment_specific": { /* * If you set the "ENVIRONMENT" environment variable, diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 873f786..c64084b 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -792,6 +792,31 @@ var BabelExt = (function(Global, unsafeGlobal) { }, + /* + * resources functions + */ + resources: { + /* + * resources.get - returns resource for [key] + */ + get: function(key) { + if ( !BabelExt.resources._resources.hasOwnProperty(key) ) throw "No such resource: " + key; + return BabelExt.resources._resources[key]; + }, + /* + * resources.set - sets resource for [key] to [value] + */ + set: function(key, value) { + BabelExt.resources._resources[key] = value; + }, + /* + * resources.remove - deletes resource item at [key] + */ + remove: function(key) { + delete BabelExt.resources._resources[key]; + } + }, + /* * storage functions */ diff --git a/res/extension.html b/res/extension.html new file mode 100644 index 0000000..e9d9c84 --- /dev/null +++ b/res/extension.html @@ -0,0 +1,9 @@ + + + BabelExt Resources + + +

BabelExt Resources

+

BabelExt.resources allows you to store and retrieve information in flat files. You can use this for anything, but it's most useful for storing large HTML snippets that would be hard to read in JavaScript.

+ + diff --git a/script/build.js b/script/build.js index ab617cd..c6d1108 100755 --- a/script/build.js +++ b/script/build.js @@ -97,6 +97,43 @@ function hardLink( source, target ) { } } +/* + * Return information about the specified files. + * Currently returns an array of { name: ..., id: ..., modified: ... } + * 'name' is the passed-in name, 'id' is the file's inode, and 'modified' is the modification time relative to the epoch. + */ +function stat( files, callback ) { + // TODO: no idea how you'd do this on Windows + files = files.filter( fs.exists ); + childProcess.execFile( 'stat', [ '--printf=%i %Y\n' ].concat(files), null, function(err,stdout,stderr) { + var lines = stdout.split("\n"); + lines.pop(); // eat trailing newline + callback( lines.map(function(line, index) { + var rows = line.split(' '); + return { name: files[index], id: rows[0], modified: rows[1] }; + }) ); + }); +} + +/* + * Build a resources.js file + */ +function build_resources() { + if ( settings.resources ) { + var resources = {}; + settings.resources.forEach(function(filename) { + resources[filename] = fs.open(filename, 'r').read(); + }); + fs.write( + 'lib/BabelExtResources.js', "BabelExt.resources._resources = " + + // prettify our JavaScript a bit, for the benefit of reviewers: + JSON.stringify(resources, null, ' ').replace( /\\n(?!")/g, "\\n\" +\n \"" ) + ";\n", + 'w' + ); + return true; + } +} + /* * Utility functions for pages */ @@ -336,70 +373,74 @@ var program_counter = new AsyncCounter(function(errors) { phantom.exit(errors||0 * Load settings from conf/settings.json */ var settings; -try { - settings = eval('('+fs.read('conf/settings.json')+')'); -} catch (e) { - console.error( - "Error in conf/settings.json: " + e + "\n" + - "Please make sure the file is formatted correctly and try again." - ); - phantom.exit(1); -} -if ( system.env.hasOwnProperty('ENVIRONMENT') ) { - var environment_specific = settings.environment_specific[ system.env.ENVIRONMENT ]; - if ( !environment_specific ) { - console.log( - 'Please specify one of the following build environments: ' + - Object.keys(settings.environment_specific).join(' ') +function update_settings() { + + try { + settings = eval('('+fs.read('conf/settings.json')+')'); + } catch (e) { + console.error( + "Error in conf/settings.json: " + e + "\n" + + "Please make sure the file is formatted correctly and try again." ); phantom.exit(1); } - Object.keys(environment_specific) - .forEach(function(property, n, properties) { - settings[ property ] = - ( Object.prototype.toString.call( settings[ property ] ) === '[object Array]' ) - ? settings[ property ].concat( environment_specific[property] ) - : environment_specific[property] - ; - }); -} else if ( settings.environment_specific ) { - console.log( - 'Please specify build environment using the ENVIRONMENT environment variable,\n' + - 'or comment out the "environment_specific" section in settings.json' - ); - phantom.exit(1); -}; -settings.contentScriptFiles.unshift('lib/BabelExt.js'); -delete settings.environment_specific; - -if ( - settings.version.search(/^[0-9]+(?:\.[0-9]+){0,3}$/) || - settings.version.split('.').filter(function(number) { return number > 65535 }).length -) { - console.log( - 'Google Chrome will not accept version number "' + settings.version + '"\n' + - 'Please specify a version number containing 1-4 dot-separated integers between 0 and 65535' - ); - phantom.exit(1); -} + if ( system.env.hasOwnProperty('ENVIRONMENT') ) { + var environment_specific = settings.environment_specific[ system.env.ENVIRONMENT ]; + if ( !environment_specific ) { + console.log( + 'Please specify one of the following build environments: ' + + Object.keys(settings.environment_specific).join(' ') + ); + phantom.exit(1); + } + Object.keys(environment_specific) + .forEach(function(property, n, properties) { + settings[ property ] = + ( Object.prototype.toString.call( settings[ property ] ) === '[object Array]' ) + ? settings[ property ].concat( environment_specific[property] ) + : environment_specific[property] + ; + }); + } else if ( settings.environment_specific ) { + console.log( + 'Please specify build environment using the ENVIRONMENT environment variable,\n' + + 'or comment out the "environment_specific" section in settings.json' + ); + phantom.exit(1); + }; + settings.contentScriptFiles.unshift('lib/BabelExt.js'); + delete settings.environment_specific; -settings.preferences.forEach(function(preference) { - /* - * Known-but-unsupported types: - * color - not supported by Safari - * file - not supported by Safari - * directory - not supported by Safari - * control - not supported by Safari, not clear what we'd do with it anyway - */ - if ( preference.type.search(/^(bool|boolint|integer|string|menulist|radio)$/) == -1 ) { + if ( + settings.version.search(/^[0-9]+(?:\.[0-9]+){0,3}$/) || + settings.version.split('.').filter(function(number) { return number > 65535 }).length + ) { console.log( - 'Preference type "' + preference.type + ' is not supported.\n' + - 'Please specify a valid preference type: bool, boolint, integer, string, menulist, radio\n' + 'Google Chrome will not accept version number "' + settings.version + '"\n' + + 'Please specify a version number containing 1-4 dot-separated integers between 0 and 65535' ); phantom.exit(1); } -}); + settings.preferences.forEach(function(preference) { + /* + * Known-but-unsupported types: + * color - not supported by Safari + * file - not supported by Safari + * directory - not supported by Safari + * control - not supported by Safari, not clear what we'd do with it anyway + */ + if ( preference.type.search(/^(bool|boolint|integer|string|menulist|radio)$/) == -1 ) { + console.log( + 'Preference type "' + preference.type + ' is not supported.\n' + + 'Please specify a valid preference type: bool, boolint, integer, string, menulist, radio\n' + ); + phantom.exit(1); + } + }); + +} +update_settings(); /* * Load settings from conf/local_settings.json @@ -1420,6 +1461,65 @@ function release_safari() { ); } +/* + * MAINTAIN COMMANDS + */ + +function maintain() { + + program_counter.begin(); + + function maintain_resources() { + update_settings(); + if ( settings.resources ) + stat( [ 'lib/BabelExtResources.js' ].concat( settings.resources ), function(files) { + var resources_file = files.shift(); + if ( files.filter(function(file) { return file.modified > resources_file.modified } ).length ) { + console.log( 'Rebuilding ' + 'lib/BabelExtResources.js' ); + build_resources(); + } + maintain_content_files(); + }); + else + maintain_content_files(); + } + + function maintain_content_files() { + var files = settings.contentScriptFiles.concat( settings.contentStyleFiles || [] ); + files = files.concat( + files.map(function(name) { return 'build/Chrome/' + name }) + ).concat( + files.map(function(name) { return 'build/Safari.safariextension/' + name }) + ); + + stat( files, function(files) { + var id_links = {}, name_links = {}; // list of file IDs that are valid hardlink targets + files.forEach(function(file) { + if ( file.name.search( '^build/' ) == -1 ) { + // source file - set hardlink target + id_links[ file.id ] = file; + name_links[ file.name ] = file; + } else if ( !id_links.hasOwnProperty(file.id) ) { + var source = name_links[ file.name.replace( /^build\/(?:[^\/]+)\//, '' ) ]; + // need to recreate + if ( file.modified > source.modified ) { + console.log( file.name + ' is newer than ' + source.name + ' - please save the built contents back to the original' ); + } else { + console.log( 'Relinking ' + file.name + ' to ' + source.name ); + fs.remove( file.name ); + hardLink( source.name, file.name ); + } + } + }); + + }); + } + + maintain_resources(); + setInterval(maintain_resources, settings.maintenanceInterval ); + +} + /* * MAIN SECTION */ @@ -1437,11 +1537,12 @@ function usage() { } program_counter.begin(); -if ( args.length != 3 ) usage(); switch ( args[1] || '' ) { case 'build': + if ( args.length != 3 ) usage(); + if ( build_resources() ) settings.contentScriptFiles.splice(1, 0, 'lib/BabelExtResources.js'); switch ( args[2] ) { case 'firefox': build_firefox(local_settings. amo_login_info); break; case 'chrome' : build_chrome (local_settings.chrome_login_info); break; @@ -1450,6 +1551,7 @@ case 'build': break; case 'release': + if ( args.length != 3 ) usage(); switch ( args[2] ) { case 'amo' : release_amo (local_settings. amo_login_info); break; case 'chrome': release_chrome(local_settings.chrome_login_info); break; @@ -1458,6 +1560,11 @@ case 'release': } break; +case 'maintain': + + maintain(); + break; + default: usage(); diff --git a/src/extension.js b/src/extension.js index 67ce8ed..9e3279a 100644 --- a/src/extension.js +++ b/src/extension.js @@ -136,6 +136,41 @@ }); } + var setResValueForm = document.getElementById('setResValueForm'); + if (setResValueForm) { + setResValueForm.addEventListener('submit', function(e) { + e.preventDefault(); + var keyEle = document.getElementById('setResKey'); + var valueEle = document.getElementById('setResValue'); + if (keyEle && valueEle) { + var key = keyEle.value; + var val = valueEle.value; + if ( key == 'myBool' ) val = !!val + else if ( key != 'myRadio' && key != 'myString' ) val = parseInt(val,10); + BabelExt.resources.set(key, val ); + var keyEle = document.getElementById('setResKey'); + var valueEle = document.getElementById('setResValue'); + keyEle.value = ''; + valueEle.value = ''; + alert('value set successfully'); + } + }); + } + + var getResValueForm = document.getElementById('getResValueForm'); + if (getResValueForm) { + getResValueForm.addEventListener('submit', function(e) { + e.preventDefault(); + var keyEle = document.getElementById('getResKey'); + if (keyEle) { + var key = keyEle.value; + var val = BabelExt.resources.get(key); + var retreivedResValueEle = document.getElementById('retreivedresvalue'); + retreivedResValueEle.innerHTML = val; + } + }); + } + var createTabForm = document.getElementById('createTabForm'); if (createTabForm) { createTabForm.addEventListener('submit', function(e) { From f0313b9caabd9b63feccb48e570901f0c9dc4ac0 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 19 Feb 2015 07:35:58 +0000 Subject: [PATCH 074/105] Fix directory location in build.js --- script/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/build.js b/script/build.js index c6d1108..36b1809 100755 --- a/script/build.js +++ b/script/build.js @@ -907,7 +907,7 @@ function build_firefox() { symbolicLink( file, 'build/firefox-unpacked/resources/'+settings.name+'/data/'+file ) }); fs.changeWorkingDirectory('build/firefox-unpacked'); - childProcess.execFile( 'zip', ['../'+xpi,'install.rdf'], null, function(err,stdout,stderr) { + childProcess.execFile( 'zip', ['../../'+xpi,'install.rdf'], null, function(err,stdout,stderr) { fs.changeWorkingDirectory('../..'); if ( stderr != '' ) { console.log(stderr.replace(/\n$/,'')); return program_counter.end(1); } console.log('Built ' + xpi + '\n\033[1mRemember to restart Firefox if you added/removed any files!\033[0m'); From 49e89bc89cf46a6affb4adbcc33c3919b9b304b1 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Thu, 19 Feb 2015 07:37:03 +0000 Subject: [PATCH 075/105] Fix typo in build.js --- script/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/build.js b/script/build.js index 36b1809..6f75b12 100755 --- a/script/build.js +++ b/script/build.js @@ -521,7 +521,7 @@ function build_safari(login_info) { '9': 'j', 'a': 'k', 'b': 'l', - 'g': 'm', + 'c': 'm', 'd': 'n', 'e': 'o', 'f': 'p', From efecaa19fe18f90ca1048c36a6776fa1efac4547 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Tue, 24 Feb 2015 09:44:19 +0000 Subject: [PATCH 076/105] Minor improvement to release_chrome --- script/build.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/script/build.js b/script/build.js index 6f75b12..4815ec3 100755 --- a/script/build.js +++ b/script/build.js @@ -1330,7 +1330,9 @@ function release_chrome(login_info) { page.waitForElementsPresent( '#code', function() { - get_auth_key( page.evaluate(function() { return document.getElementById('code').value }) ); + var code = page.evaluate(function() { return document.getElementById('code').value }); + var post_data = "grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id=" + login_info.client_id + "&client_secret=" + login_info.client_secret + "&code=" + code; + page.openBinary( 'https://accounts.google.com/o/oauth2/token', { data: post_data }, upload_and_publish ); } ) }, @@ -1342,12 +1344,7 @@ function release_chrome(login_info) { } ); - function get_auth_key(code) { - var post_data = "grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob&client_id=" + login_info.client_id + "&client_secret=" + login_info.client_secret + "&code=" + code; - page.openBinary( 'https://accounts.google.com/o/oauth2/token', { data: post_data }, upload_and_publish ); - } - - function upload_and_publish(data) { + function upload_and_publish(err, data) { data = JSON.parse(data); From 55d481ca11702b3798c0c814e2855f8fae369131 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Tue, 24 Feb 2015 09:45:59 +0000 Subject: [PATCH 077/105] Support arbitrary directory names (including arbitrarily nested directory trees) --- script/build.js | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/script/build.js b/script/build.js index 4815ec3..13eb018 100755 --- a/script/build.js +++ b/script/build.js @@ -97,6 +97,22 @@ function hardLink( source, target ) { } } +/* + * Create a tree of directories + */ +function makeTree( directory ) { + directory = directory.split( '/' ); + for ( var n=0; n!=directory.length; ++n ) { + if ( !fs.exists( directory.slice( 0, n ).join( '/' ) ) ) + fs.makeDirectory(directory.slice( 0, n ).join( '/' )); + } +} + +// Sugar functions to make the containing directory and file link: +function makeTreeHardLink ( source, target ) { makeTree( target.replace( /[^\/]+$/, '' ) ); hardLink( source, target ); } +function makeTreeSymbolicLink( source, target ) { makeTree( target.replace( /[^\/]+$/, '' ) ); symbolicLink( source, target ); } + + /* * Return information about the specified files. * Currently returns an array of { name: ..., id: ..., modified: ... } @@ -557,12 +573,9 @@ function build_safari(login_info) { while (start_scripts.firstChild) start_scripts.removeChild(start_scripts.firstChild); while ( end_scripts.firstChild) end_scripts.removeChild( end_scripts.firstChild); - fs.makeDirectory('build/Safari.safariextension/icons'); - fs.makeDirectory('build/Safari.safariextension/src'); - fs.makeDirectory('build/Safari.safariextension/lib'); - settings.contentScriptFiles.forEach(function(file) { - hardLink( file, 'build/Safari.safariextension/' + file ) + + makeTreeHardLink( file, 'build/Safari.safariextension/' + file ) var script = document.createElement("string"); script.textContent = file; @@ -584,7 +597,7 @@ function build_safari(login_info) { while (stylesheets.firstChild) stylesheets.removeChild(stylesheets.firstChild); settings.contentStyleFiles.forEach(function(file) { - hardLink( file, 'build/Safari.safariextension/' + file ) + makeTreeHardLink( file, 'build/Safari.safariextension/' + file ) var sheet = document.createElement("string"); sheet.textContent = file; @@ -808,13 +821,12 @@ function build_firefox() { // Copy scripts into place: fs.removeTree('build/Firefox/data'); // PhantomJS won't list dangling symlinks, so we have to just delete the directory and recreate it fs.makeDirectory('build/Firefox/data'); - fs.makeDirectory('build/Firefox/data/src'); - fs.makeDirectory('build/Firefox/data/lib'); - fs.makeDirectory('build/Firefox/icons'); var contentFiles = settings.contentScriptFiles.concat( settings.contentStyleFiles || [] ); - contentFiles.forEach(function(file) { symbolicLink( file, 'build/Firefox/data/' + file ) }); + contentFiles.forEach(function(file) { + makeTreeSymbolicLink( file, 'build/Firefox/data/' + file ); + }); // Create settings.js: fs.write( @@ -845,8 +857,8 @@ function build_firefox() { "id": settings.id, "name": settings.name }; - if (settings.icons[48] ) { pkg.icon = settings.icons[48]; symbolicLink( pkg.icon , 'build/Firefox/'+pkg.icon ); } - if (settings.icons[64] ) { pkg.icon_64 = settings.icons[64]; symbolicLink( pkg.icon_64, 'build/Firefox/'+pkg.icon_64 ); } + if (settings.icons[48] ) { pkg.icon = settings.icons[48]; makeTreeSymbolicLink( pkg.icon , 'build/Firefox/'+pkg.icon ); } + if (settings.icons[64] ) { pkg.icon_64 = settings.icons[64]; makeTreeSymbolicLink( pkg.icon_64, 'build/Firefox/'+pkg.icon_64 ); } if (settings.preferences) { pkg.preferences = settings.preferences; } fs.write( 'build/Firefox/package.json', JSON.stringify(pkg, null, ' ' ) + "\n", 'w' ); @@ -904,7 +916,7 @@ function build_firefox() { ); contentFiles.forEach(function(file) { fs.remove('build/firefox-unpacked/resources/'+settings.name+'/data/'+file); - symbolicLink( file, 'build/firefox-unpacked/resources/'+settings.name+'/data/'+file ) + makeTreeSymbolicLink( file, 'build/firefox-unpacked/resources/'+settings.name+'/data/'+file ) }); fs.changeWorkingDirectory('build/firefox-unpacked'); childProcess.execFile( 'zip', ['../../'+xpi,'install.rdf'], null, function(err,stdout,stderr) { @@ -1043,12 +1055,8 @@ function build_chrome() { // Create manifest.json: fs.write( 'build/Chrome/manifest.json', JSON.stringify(manifest, null, '\t' ) + "\n", 'w' ); - fs.makeDirectory('build/Chrome/icons'); - fs.makeDirectory('build/Chrome/src'); - fs.makeDirectory('build/Chrome/lib'); - // Copy scripts and icons into place: - contentFiles.forEach(function(file) { hardLink( file , 'build/Chrome/' + file ) }); + contentFiles.forEach(function(file) { makeTreeHardLink( file, 'build/Chrome/' + file ) }); program_counter.begin(); From c51a9a14355ae4be6aa334cce11862f5b2ac795f Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 6 Mar 2015 16:18:10 +0000 Subject: [PATCH 078/105] Cosmetic code fix in build.js --- script/build.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/script/build.js b/script/build.js index 13eb018..e3fd2d4 100755 --- a/script/build.js +++ b/script/build.js @@ -1476,17 +1476,18 @@ function maintain() { function maintain_resources() { update_settings(); - if ( settings.resources ) + if ( settings.resources ) { stat( [ 'lib/BabelExtResources.js' ].concat( settings.resources ), function(files) { var resources_file = files.shift(); if ( files.filter(function(file) { return file.modified > resources_file.modified } ).length ) { - console.log( 'Rebuilding ' + 'lib/BabelExtResources.js' ); + console.log( 'Rebuilding lib/BabelExtResources.js' ); build_resources(); } maintain_content_files(); }); - else + } else { maintain_content_files(); + } } function maintain_content_files() { From 0a630d2cd351505004509b6ca587391684b761d4 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 6 Mar 2015 16:18:19 +0000 Subject: [PATCH 079/105] Fix help text in build.js --- script/build.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/build.js b/script/build.js index e3fd2d4..dbfe916 100755 --- a/script/build.js +++ b/script/build.js @@ -1537,7 +1537,8 @@ function usage() { 'Usage: ' + args[0] + ' \n' + 'Commands:\n' + ' build - builds extensions for "amo" (Firefox) "chrome" or "safari"\n' + - ' release - release extension to "amo" (addons.mozilla.org) "chrome" (Chrome store), "opera" (opera site) or "safari" (extensions gallery)' + ' release - release extension to "amo" (addons.mozilla.org) "chrome" (Chrome store), "opera" (opera site) or "safari" (extensions gallery)\n' + + ' maintain - keep various files up-to-date' ); phantom.exit(1); } From 1484d36223342f4a92d8c2d1b8b7766cc39b1140 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 12 Apr 2015 11:31:25 +0100 Subject: [PATCH 080/105] Readability improvements --- script/build.js | 50 +++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/script/build.js b/script/build.js index dbfe916..2684545 100755 --- a/script/build.js +++ b/script/build.js @@ -19,6 +19,11 @@ var fs = require('fs'); var system = require('system'); var webPage = require('webpage'); +/* + * OS INTERACTION + * improve PhantomJS' ability to interact with the operating system + */ + var chrome_command = ( system.os.name == 'windows' ) ? 'chrome.exe' @@ -132,26 +137,8 @@ function stat( files, callback ) { } /* - * Build a resources.js file - */ -function build_resources() { - if ( settings.resources ) { - var resources = {}; - settings.resources.forEach(function(filename) { - resources[filename] = fs.open(filename, 'r').read(); - }); - fs.write( - 'lib/BabelExtResources.js', "BabelExt.resources._resources = " + - // prettify our JavaScript a bit, for the benefit of reviewers: - JSON.stringify(resources, null, ' ').replace( /\\n(?!")/g, "\\n\" +\n \"" ) + ";\n", - 'w' - ); - return true; - } -} - -/* - * Utility functions for pages + * PAGE UTILITIES + * Functions to better interact with web pages */ function _waitForEvent( test, callback ) { // low-level interface - see waitFor* below @@ -371,6 +358,10 @@ function page( url, callback ) { }); } +/* + * MISCELLANEOUS BABELEXT-SPECIFIC UTILITIES + */ + /* * Keep track of asynchronous jobs, and exit when the last one finishes: */ @@ -385,6 +376,25 @@ AsyncCounter.prototype.end = function(errors) { this.errors += (errors||0); if var program_counter = new AsyncCounter(function(errors) { phantom.exit(errors||0) }); +/* + * Build a resources.js file + */ +function build_resources() { + if ( settings.resources ) { + var resources = {}; + settings.resources.forEach(function(filename) { + resources[filename] = fs.open(filename, 'r').read(); + }); + fs.write( + 'lib/BabelExtResources.js', "BabelExt.resources._resources = " + + // prettify our JavaScript a bit, for the benefit of reviewers: + JSON.stringify(resources, null, ' ').replace( /\\n(?!")/g, "\\n\" +\n \"" ) + ";\n", + 'w' + ); + return true; + } +} + /* * Load settings from conf/settings.json */ From 8a54b66c903446b857ef2d69903733d186203786 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 17 Apr 2015 12:33:44 +0100 Subject: [PATCH 081/105] Support auto-reloading in Chrome --- build/Chrome/background.js | 16 ++++++++++++++++ conf/settings.json | 12 ++++++++++++ script/build.js | 5 ++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/build/Chrome/background.js b/build/Chrome/background.js index 26e448a..f85d665 100644 --- a/build/Chrome/background.js +++ b/build/Chrome/background.js @@ -153,3 +153,19 @@ chrome.runtime.onConnect.addListener(function(port) { }); } }); + +if ( auto_reload ) { + // Chrome defines a "fast reload" as a reload within 10 seconds of the previous one. + // Five consecutive fast reloads and the extension is disabled for a little while. + // Ideally we'd allow a small burst, but that would require us to store the burst count across reloads + var reload_timeout = new Date().getTime() + 10000; + chrome.webNavigation.onBeforeNavigate.addListener(function(data) { + var time = reload_timeout - new Date().getTime() + if ( time < 0 ) { + chrome.runtime.reload(); + reload_timeout = new Date().getTime() + 10000; + } else { + console.log( 'Fast reload detected - must wait ' + (time/1000) + ' seconds before reloading the extension again' ); + } + }); +} diff --git a/conf/settings.json b/conf/settings.json index 11ebea3..c318796 100644 --- a/conf/settings.json +++ b/conf/settings.json @@ -54,7 +54,19 @@ * variables from the relevant block will be used: */ "development": { + "contentScriptFiles": [], + + /* + * Reloads unpacked extensions as frequently as possible. Currently only supports + * Chrome/Opera, which can be forced to reload extensions so long as we avoid their + * "fast reload" protection. Note that Firefox reloads unpacked extensions automatically. + * + * Valid values: + * + 'timeout' - reload as frequently as possible without triggering fast reloads + */ + "autoReload": 'timeout', + }, "test": { diff --git a/script/build.js b/script/build.js index 2684545..dfc6c98 100755 --- a/script/build.js +++ b/script/build.js @@ -1004,6 +1004,8 @@ function build_chrome() { fs.remove ('build/Chrome/' + file); }); + if ( settings.autoReload ) manifest.permissions.push('webNavigation'); + fs.write( 'build/Chrome/' + manifest.background.scripts[0], "var default_preferences = {" + @@ -1014,7 +1016,8 @@ function build_chrome() { default : return "'" + preference.name + "':" + JSON.stringify(preference.value); } }).join(', ') + - "};\n", + "};\n" + + "var auto_reload = " + (settings.autoReload == 'timeout') + ";\n", 'w' ); From 1e1df3b3b0c845523f03ff4094e43672b8f42c88 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 17 Apr 2015 12:45:22 +0100 Subject: [PATCH 082/105] Add warning message when passing an unknown build/release target --- script/build.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/build.js b/script/build.js index dfc6c98..d6d4a99 100755 --- a/script/build.js +++ b/script/build.js @@ -1567,6 +1567,7 @@ case 'build': case 'firefox': build_firefox(local_settings. amo_login_info); break; case 'chrome' : build_chrome (local_settings.chrome_login_info); break; case 'safari' : build_safari (local_settings.safari_login_info); break; + default : console.log( "Please specify 'firefox', 'chrome' or 'safari', not '" + args[2] + "'" ); break; } break; @@ -1577,6 +1578,7 @@ case 'release': case 'chrome': release_chrome(local_settings.chrome_login_info); break; case 'opera' : release_opera (local_settings. opera_login_info); break; case 'safari': release_safari(local_settings.safari_login_info); break; + default : console.log( "Please specify 'amo', 'chrome', 'opera' or 'safari', not '" + args[2] + "'" ); break; } break; From 91275a6a87e90d8ce4953352896c2773411f2638 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 17 Apr 2015 12:59:44 +0100 Subject: [PATCH 083/105] Add BabelExt.about --- script/build.js | 14 +++++++++++--- src/extension.js | 3 +++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/script/build.js b/script/build.js index d6d4a99..86db267 100755 --- a/script/build.js +++ b/script/build.js @@ -380,6 +380,11 @@ var program_counter = new AsyncCounter(function(errors) { phantom.exit(errors||0 * Build a resources.js file */ function build_resources() { + var about = "BabelExt.about = " + JSON.stringify({ + version : settings.version, + build_time: new Date().toString() + }) + ";\n"; + if ( settings.resources ) { var resources = {}; settings.resources.forEach(function(filename) { @@ -388,10 +393,12 @@ function build_resources() { fs.write( 'lib/BabelExtResources.js', "BabelExt.resources._resources = " + // prettify our JavaScript a bit, for the benefit of reviewers: - JSON.stringify(resources, null, ' ').replace( /\\n(?!")/g, "\\n\" +\n \"" ) + ";\n", + JSON.stringify(resources, null, ' ').replace( /\\n(?!")/g, "\\n\" +\n \"" ) + ";\n" + + about, 'w' ); - return true; + } else { + fs.write( 'lib/BabelExtResources.js', about, 'w' ); } } @@ -1562,7 +1569,8 @@ switch ( args[1] || '' ) { case 'build': if ( args.length != 3 ) usage(); - if ( build_resources() ) settings.contentScriptFiles.splice(1, 0, 'lib/BabelExtResources.js'); + build_resources(); + settings.contentScriptFiles.splice(1, 0, 'lib/BabelExtResources.js'); switch ( args[2] ) { case 'firefox': build_firefox(local_settings. amo_login_info); break; case 'chrome' : build_chrome (local_settings.chrome_login_info); break; diff --git a/src/extension.js b/src/extension.js index 9e3279a..c90fa7d 100644 --- a/src/extension.js +++ b/src/extension.js @@ -240,6 +240,9 @@ } }); + // Information about the extension (e.g. version, build time) + console.log( BabelExt.about ); + /* * Dispatcher * From a483d14a131a98c80180f3a182b6607dc81e86ce Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Fri, 17 Apr 2015 13:38:10 +0100 Subject: [PATCH 084/105] Add BabelExt.debugLog --- lib/BabelExt.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ src/extension.js | 5 +++ 2 files changed, 85 insertions(+) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index c64084b..f60df95 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -922,3 +922,83 @@ var BabelExt = (function(Global, unsafeGlobal) { detectedBrowser: instance.detectedBrowser }; })(Global, unsafeGlobal); + +BabelExt.debugLog = function() { + // "use strict" // NO! + // We need to show "log.caller" in the debug log, which is not available in strict mode + + var start_time = new Date(); + + var div = document.createElement('DIV'); + div.setAttribute( 'style', "width: 100%; text-align: center" ); + div.innerHTML = '

Debugging log - please send this to the maintainer if requested

'; + + var textarea = div.lastChild; + + return { + + /** + * @summary Show the debugging log at the bottom of the current page + * @description You might want to show the debugging log at page load, or only when a "severe" error occurs + */ + show: function() { + if ( document.body ) { + document.body.appendChild(div); + } else { + var interval = setInterval(function() { + if ( document.body ) { + document.body.appendChild(div); + clearInterval(interval); + } + }, 10 ); + } + this.show = function() { return this }; + return this; + }, + + /** + * @summary Add values to the debugging log + * @param {...Any} var_args variadic list of things to log + * Need to define this eoutside the main block so we can build a stack trace with "log.caller" + * ("use strict" breaks this) + */ + log: function log() { + var messages = []; + for ( var n=0; n!=arguments.length; ++n ) try { + messages.push( JSON.stringify(arguments[n]) ); + } catch (e) { + messages.push( '(cannot stringify circular data structure)' ); + } + var caller = log.caller.name || log.caller.toString(); + caller = caller.length > 110 ? caller.substr(0,100) + '...' : caller; + BabelExt.utils.runInEmbeddedPage( 'document.head.setAttribute("data-js-is-enabled", "true" );' ); + textarea.value += + '================================================================================\n' + + 'Start date: ' + start_time + " (" + ( new Date().getTime() - start_time.getTime() )/1000 + " seconds ago)\n" + + 'Extension version: ' + BabelExt.about.version + ' (built: ' + BabelExt.about.build_time + ')\n' + + 'Frame: ' + ( (window.location == window.parent.location) ? 'main document' : 'iFrame' ) + "\n" + + 'URL: ' + location.toString() + "\n" + + 'User agent: ' + navigator.userAgent + "\n" + + 'Cookies: ' + ( + ( typeof(navigator.cookieEnabled) == 'undefined' ) + ? 'unknown' + : ( navigator.cookieEnabled ? 'enabled' : 'disabled' ) + ) + "\n" + + 'Javascript: ' + ( document.head.hasAttribute('data-js-is-enabled') ? 'enabled' : 'disabled' ) + "\n" + + 'Caller: ' + caller + "\n" + + "Data: [\n " + messages.join(",\n ") + "\n]\n" + + "\n" + ; + document.head.removeAttribute('data-js-is-enabled'); + }, + + /** + * @summary Get the contents of the debug log + */ + text: function() { + return '' + textarea.value; + } + + } + +} diff --git a/src/extension.js b/src/extension.js index c90fa7d..4ddb378 100644 --- a/src/extension.js +++ b/src/extension.js @@ -243,6 +243,11 @@ // Information about the extension (e.g. version, build time) console.log( BabelExt.about ); + // Simple framework for logging issues to the page: + var debugLog = BabelExt.debugLog(); + debugLog.log( 'hello, world!' ); + debugLog.show(); + /* * Dispatcher * From da1b56cee485b5ceb92b04cf7e40fc9c3f9c7b48 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sat, 18 Apr 2015 13:44:54 +0100 Subject: [PATCH 085/105] Add BabelExt.XMLHttpRequest() --- build/Chrome/background.js | 19 ++-- build/Firefox/lib/main.js | 43 ++++----- build/Safari.safariextension/background.html | 15 ++-- conf/settings.json | 6 ++ lib/BabelExt.js | 95 +++++++++++++++++++- script/build.js | 30 ++++++- src/extension.js | 16 ++++ 7 files changed, 181 insertions(+), 43 deletions(-) diff --git a/build/Chrome/background.js b/build/Chrome/background.js index f85d665..5eeb89b 100644 --- a/build/Chrome/background.js +++ b/build/Chrome/background.js @@ -12,19 +12,22 @@ chrome.runtime.onMessage.addListener( switch(request.requestType) { case 'xmlhttpRequest': var xhr = new XMLHttpRequest(); - xhr.open(request.method, request.url, true); + xhr.open(request.method, request.url, true, request.user, request.password); if (request.method === "POST") { xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); // xhr.setRequestHeader("Content-length", request.data.length); // xhr.setRequestHeader("Connection", "close"); } - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - // Only store 'status' and 'responseText' fields and send them back. - var response = {status: xhr.status, responseText: xhr.responseText}; - sendResponse(response); - } - }; + Object.keys(request.headers).forEach(function(header) { xhr.setRequestHeader(header, request.headers[header]) }); + if ( typeof(request.overrideMimeType) != 'undefined' ) xhr.overrideMimeType = request.overrideMimeType; + xhr.onload = function() { + var response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders()}; + sendResponse(response); + } + xhr.onerror = function() { + var response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders(), error: true}; + sendResponse(response); + } xhr.send(request.data); return true; // true must be returned here to indicate successful XHR break; diff --git a/build/Firefox/lib/main.js b/build/Firefox/lib/main.js index 48558b6..aebfc1a 100644 --- a/build/Firefox/lib/main.js +++ b/build/Firefox/lib/main.js @@ -1,7 +1,7 @@ // Import the APIs we need. var pageMod = require("sdk/page-mod"); -var Request = require("sdk/request").Request; +var XMLHttpRequest = require("sdk/net/xhr").XMLHttpRequest; var notifications = require("sdk/notifications"); var self = require("sdk/self"); var tabs = require("sdk/tabs"); @@ -70,33 +70,22 @@ pageMod.PageMod({ callbackID: request.callbackID, name: request.requestType }; - if (request.method == 'POST') { - Request({ - url: request.url, - onComplete: function(response) { - responseObj.response = { - responseText: response.text, - status: response.status - }; - worker.postMessage(responseObj); - }, - headers: request.headers, - content: request.data - }).post(); - } else { - Request({ - url: request.url, - onComplete: function(response) { - responseObj.response = { - responseText: response.text, - status: response.status - }; - worker.postMessage(responseObj); - }, - headers: request.headers, - content: request.data - }).get(); + var xhr = new XMLHttpRequest(); + xhr.open(request.method, request.url, true, request.user, request.password); + if (request.method === "POST") { + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); } + Object.keys(request.headers).forEach(function(header) { xhr.setRequestHeader(header, request.headers[header]) }); + if ( typeof(request.overrideMimeType) != 'undefined' ) xhr.overrideMimeType = request.overrideMimeType; + xhr.onload = function() { + responseObj.response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders()}; + worker.postMessage(responseObj); + } + xhr.onerror = function() { + responseObj.response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders(), error: true}; + worker.postMessage(responseObj); + } + xhr.send(request.data); break; case 'createTab': var focus = (request.background !== true); diff --git a/build/Safari.safariextension/background.html b/build/Safari.safariextension/background.html index be36da3..d06f84f 100644 --- a/build/Safari.safariextension/background.html +++ b/build/Safari.safariextension/background.html @@ -8,14 +8,19 @@ case 'xmlhttpRequest': var xhr = new XMLHttpRequest(); xhr.XHRID = msgEvent.message.XHRID; - xhr.open(request.method, request.url, true); + xhr.open(request.method, request.url, true, request.user, request.password); if (request.method == "POST") { xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); } - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - sendResponse(xhr, msgEvent); - } + Object.keys(request.headers).forEach(function(header) { xhr.setRequestHeader(header, request.headers[header]) }); + if ( typeof(request.overrideMimeType) != 'undefined' ) xhr.overrideMimeType = request.overrideMimeType; + xhr.onload = function() { + var response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders()}; + sendResponse(response, msgEvent); + } + xhr.onerror = function() { + var response = {status: xhr.status, statusText: xhr.statusText, responseText: xhr.responseText, _response_headers: xhr.getAllResponseHeaders(), error: true}; + sendResponse(response, msgEvent); } xhr.send(request.data); break; diff --git a/conf/settings.json b/conf/settings.json index c318796..bbc9c0d 100644 --- a/conf/settings.json +++ b/conf/settings.json @@ -46,6 +46,12 @@ // whether to match https://: "match_secure_domain": true, + "xhr_patterns": [ + // allowed URL patterns for cross-site/mixed content XMLHttpRequests + // See https://developer.chrome.com/extensions/match_patterns + "http://www.w3.org/*" + ], + "maintenanceInterval": 5 * 1000, // the 'maintain' command will pause for this long between updates "environment_specific": { diff --git a/lib/BabelExt.js b/lib/BabelExt.js index f60df95..7ccfa62 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -232,6 +232,7 @@ var BabelExt = (function(Global, unsafeGlobal) { } /* + * XHR functionality based on the GM_xmlhttpRequest API * check the xmlHttpRequest function to see if it's cross domain, and pass to * the background page if appropriate. If it's not, run as a normal xmlHttpRequest. * xmlhttpRequest = function(obj) { @@ -239,6 +240,8 @@ var BabelExt = (function(Global, unsafeGlobal) { instance.BabelXHR = function(obj) { var crossDomain = (location) ? (obj.url.indexOf(location.hostname) === -1) : true; if (crossDomain) { + if ( obj.url.search(BabelExt._xhr_regexp) == -1 ) + throw "Please add '" + url + "' to xhr_patterns in your extension settings"; obj.requestType = 'xmlhttpRequest'; if (typeof(obj.onload) === 'undefined') { obj.onload = function() {}; // anon function that does nothing, since none was specified... @@ -919,7 +922,97 @@ var BabelExt = (function(Global, unsafeGlobal) { } }, - detectedBrowser: instance.detectedBrowser + detectedBrowser: instance.detectedBrowser, + + /* + * XHR functionality based on the XMLHttpRequest API + * Build an XHR object that can do cross-site requests + * var xhr = new BabelExt.XMLHttpRequest(); + */ + XMLHttpRequest: function() { + this._request_headers = {}; + this.setRequestHeader = function( header, value ) { this._request_headers[header] = value }; + this.overrideMimeType = function( mime_type ) { this._mime_type = mime_type }; + this.open = function(method, url, async, user, password) { + + var xhr = this; + + if ( url == location.origin || url.search(location.origin+'/') == 0 ) { + // Single-site request - proxy everything straight through to an XMLHttpRequest object: + + this._xhr = new XMLHttpRequest( method, url, async, user, password ); + Object.keys(this._request_headers).forEach(function(header) { xhr._xhr.setRequestHeader(header, xhr._request_headers[header]) }); + if ( this.hasOwnProperty('_mime_type') ) this._xhr.overrideMimeType(this._mime_type); + + // proxy properties through to the XHR: + [ + 'onreadystatechange', 'ontimeout', 'onabort', 'onerror', 'onload', 'onloadend', 'onloadstart', 'onprogress', + 'readyState', 'responseText', 'responseType', 'status', 'statusText', 'timeout', 'withCredentials', 'response', 'responseXML' + ] + .forEach(function(property) { + if ( xhr.hasOwnProperty(property) ) { + xhr._xhr[property] = xhr[property]; + delete xhr[property]; + } + Object.defineProperty(xhr, property, { get: function() { return xhr._xhr[property] }, set: function(p) { xhr._xhr[property] = p } }); + }); + + // proxy methods through to the XHR: + [ 'abort', 'overrideMimeType', 'send', 'setRequestHeader', 'getAllResponseHeaders', 'getResponseHeader' ].forEach(function(method) { + xhr[method] = function() { return xhr._xhr[method].apply( xhr._xhr, Array.prototype.slice.call( arguments, 0 ) ) }; + }); + + xhr._xhr.open(method, url, (typeof(async)=='undefined')?true:async, user, password); + + } else { + + if ( url.search(BabelExt._xhr_regexp) == -1 ) + throw "Please add '" + url + "' to xhr_patterns in your extension settings"; + + // Communicating with the browser side is necessarily asynchronous: + if ( typeof(async) != 'undefined' && !async ) throw "Synchronous cross-site requests are not supported"; + + var aborted; + this.abort = function() { + if ( this.onabort ) this.onabort(); + aborted = true; + }; + + this._response_headers = null; + this.getAllResponseHeaders = function( ) { return this._response_headers } + this.getResponseHeader = function(h) { return ( this._response_headers || {} ).hasOwnProperty(h) ? this._response_headers[h] : null }; + + this.send = function(data) { + if ( this.onloadstart ) this.onloadstart(); + instance.bgMessage({ + requestType: 'xmlhttpRequest', + method : method, + url : url, + headers : this._request_headers, + data : data, + overrideMimeType: this._mime_type, + }, function(response) { + if ( aborted ) return; + Object.keys(response).forEach(function(p) { + xhr[p] = response[p]; + }); + xhr.readyState = 4; + xhr.responseType = 'text'; + if ( this.onreadystatechange ) this.onreadystatechange(); + if ( response.error ) { + if ( xhr.onerror ) xhr.onerror(); + } else { + if ( xhr.onload ) xhr.onload (); + } + if ( this.onloadend ) this.onloadend(); + }); + }; + + } + + } + } + }; })(Global, unsafeGlobal); diff --git a/script/build.js b/script/build.js index 86db267..785bbe7 100755 --- a/script/build.js +++ b/script/build.js @@ -384,6 +384,7 @@ function build_resources() { version : settings.version, build_time: new Date().toString() }) + ";\n"; + var xhr_regexp = "BabelExt._xhr_regexp = new RegExp('" + ( settings.xhr_regexp || '(?!)' ) + "');\n"; if ( settings.resources ) { var resources = {}; @@ -394,11 +395,11 @@ function build_resources() { 'lib/BabelExtResources.js', "BabelExt.resources._resources = " + // prettify our JavaScript a bit, for the benefit of reviewers: JSON.stringify(resources, null, ' ').replace( /\\n(?!")/g, "\\n\" +\n \"" ) + ";\n" + - about, + about + xhr_regexp, 'w' ); } else { - fs.write( 'lib/BabelExtResources.js', about, 'w' ); + fs.write( 'lib/BabelExtResources.js', about + xhr_regexp, 'w' ); } } @@ -455,6 +456,28 @@ function update_settings() { phantom.exit(1); } + if ( settings.xhr_patterns ) { + /* + * Convert a "match pattern" to a regular expression + * Different browsers implement this differently. + * We treat Chrome's implementation as canonical: https://developer.chrome.com/extensions/match_patterns + */ + var regexps = []; + settings.xhr_patterns.forEach(function(pattern) { + if ( pattern.replace( /^(\*|https?|file|ftp):\/\/(\*|(?:\*\.)?[^\/*]*)\/(.*)$/, function( url, protocol, domain, path ) { + protocol = ( protocol == '*' ) ? 'https?' : protocol.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g , "\\$&"); + domain = domain.replace( /[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g , "\\$&").replace( '*', '[^/]*' ); + path = path .replace( /[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g , "\\$&").replace( '*', '.*' ); + regexps.push( protocol + '://' + domain + '/' + path ); + return url + ' '; + }) == pattern ) { + console.log( 'Ignoring invalid match pattern: ' + pattern ); + } + }); + settings.xhr_regexp = '^(?:' + regexps.join('|') + ')$'; + } + + settings.preferences.forEach(function(preference) { /* * Known-but-unsupported types: @@ -995,6 +1018,9 @@ function build_chrome() { var extra_files = []; + if ( settings.xhr_patterns ) + manifest.permissions = settings.xhr_patterns.concat(manifest.permissions); + if ( settings.preferences ) { manifest.options_page = "options.html"; manifest.permissions.push('storage'); diff --git a/src/extension.js b/src/extension.js index 4ddb378..9af63a5 100644 --- a/src/extension.js +++ b/src/extension.js @@ -248,6 +248,22 @@ debugLog.log( 'hello, world!' ); debugLog.show(); + // XMLHttpRequest-compatible API (supports cross-site requests and mixed content): + // see also BabelExt.xhr() + var same_site_xhr = new BabelExt.XMLHttpRequest(); + same_site_xhr.open( 'GET', location.origin ); + same_site_xhr.onreadystatechange = function() { + console.log( this.readyState, this.responseText ); + } + same_site_xhr.send(); + + var cross_site_xhr = new BabelExt.XMLHttpRequest(); + cross_site_xhr.open( 'GET', 'http://www.w3.org/' ); + cross_site_xhr.onreadystatechange = function() { + console.log( this.readyState, this.responseText ); + } + cross_site_xhr.send(); + /* * Dispatcher * From ac6022ee899d9617d680fe8b07798c9b19b96fb3 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Tue, 21 Apr 2015 20:19:48 +0100 Subject: [PATCH 086/105] s/this/xhr/ in BabelExt.XMLHttpRequest.send() --- lib/BabelExt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 7ccfa62..483bf45 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -998,7 +998,7 @@ var BabelExt = (function(Global, unsafeGlobal) { }); xhr.readyState = 4; xhr.responseType = 'text'; - if ( this.onreadystatechange ) this.onreadystatechange(); + if ( xhr.onreadystatechange ) xhr.onreadystatechange(); if ( response.error ) { if ( xhr.onerror ) xhr.onerror(); } else { From 61c2bd1e677c8f849219d9f7e77b6e91baebc286 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Wed, 22 Apr 2015 15:34:44 +0100 Subject: [PATCH 087/105] s/this/xhr/ in BabelExt.XMLHttpRequest.send() (again!) --- lib/BabelExt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 483bf45..f5cdec6 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -1004,7 +1004,7 @@ var BabelExt = (function(Global, unsafeGlobal) { } else { if ( xhr.onload ) xhr.onload (); } - if ( this.onloadend ) this.onloadend(); + if ( xhr.onloadend ) xhr.onloadend(); }); }; From 2ccf75b5725e1d4c06efa2befb72e707291c7a2c Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Wed, 22 Apr 2015 16:26:13 +0100 Subject: [PATCH 088/105] Better error message when attempting a non-whitelisted cross-site XHR --- lib/BabelExt.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index f5cdec6..9450f0d 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -966,8 +966,10 @@ var BabelExt = (function(Global, unsafeGlobal) { } else { - if ( url.search(BabelExt._xhr_regexp) == -1 ) + if ( url.search(BabelExt._xhr_regexp) == -1 ) { + console.log( "Blocked request to non-whitelisted URL '" + url + "' - please add this to xhr_patterns in your extension settings" ); throw "Please add '" + url + "' to xhr_patterns in your extension settings"; + } // Communicating with the browser side is necessarily asynchronous: if ( typeof(async) != 'undefined' && !async ) throw "Synchronous cross-site requests are not supported"; From 675e942e47ffeea09bbd8a2ea9baea336102ffb7 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 26 Apr 2015 04:32:11 +0100 Subject: [PATCH 089/105] Fix BabelExt.XMLHttpRequest() for relative URLs --- lib/BabelExt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/BabelExt.js b/lib/BabelExt.js index 9450f0d..5469c99 100644 --- a/lib/BabelExt.js +++ b/lib/BabelExt.js @@ -937,7 +937,7 @@ var BabelExt = (function(Global, unsafeGlobal) { var xhr = this; - if ( url == location.origin || url.search(location.origin+'/') == 0 ) { + if ( url == location.origin || url.search(location.origin+'/') == 0 || url.search(/^[a-z]+:/) == -1 ) { // Single-site request - proxy everything straight through to an XMLHttpRequest object: this._xhr = new XMLHttpRequest( method, url, async, user, password ); From 90d6b27727623af3aa753d60413ff0e5daab788b Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Sun, 26 Apr 2015 12:45:04 +0100 Subject: [PATCH 090/105] Execute JavaScript in all frames in Chrome --- script/build.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/script/build.js b/script/build.js index 785bbe7..e2537cf 100755 --- a/script/build.js +++ b/script/build.js @@ -995,7 +995,10 @@ function build_chrome() { { "matches": match_urls, "js": settings.contentScriptFiles, - "run_at": when_string[settings.contentScriptWhen] + "run_at": when_string[settings.contentScriptWhen], + // Chrome defaults to only loading in the main frame, Firefox can only load in all frames. + // Not sure which behaviour is better, but this at least makes it standard across browsers: + "all_frames": true } ], "icons": settings.icons, From b0066b7a629dac4773bb1570d4981df74e8bad45 Mon Sep 17 00:00:00 2001 From: Andrew Sayers Date: Mon, 27 Apr 2015 00:17:46 +0100 Subject: [PATCH 091/105] Add BabelExt.memoryStorage --- build/Chrome/background.js | 20 +++++++ build/Firefox/lib/main.js | 49 +++++++++++++--- build/Safari.safariextension/background.html | 34 +++++++++++ lib/BabelExt.js | 60 ++++++++++++++++++++ sink.html | 29 ++++++++++ src/extension.js | 39 +++++++++++++ 6 files changed, 222 insertions(+), 9 deletions(-) diff --git a/build/Chrome/background.js b/build/Chrome/background.js index 5eeb89b..4cf2fc7 100644 --- a/build/Chrome/background.js +++ b/build/Chrome/background.js @@ -5,6 +5,11 @@ var contextMenuClick = function(info, tab, callbackID) { }); }; +var memoryStorage = { storage: {} }; +memoryStorage. getItem = function(key ) { return memoryStorage.storage[key] }; +memoryStorage. setItem = function(key, value) { memoryStorage.storage[key] = value }; +memoryStorage.removeItem = function(key ) { delete memoryStorage.storage[key] }; + chrome.runtime.onMessage.addListener( function(request, sender, sendResponse) { // all requests expect a JSON object with requestType and then the relevant @@ -72,6 +77,21 @@ chrome.runtime.onMessage.addListener( break; } break; + case 'memoryStorage': + switch (request.operation) { + case 'getItem': + sendResponse({status: true, key: request.itemName, value: memoryStorage.getItem(request.itemName)}); + break; + case 'removeItem': + memoryStorage.removeItem(request.itemName); + sendResponse({status: true, key: request.itemName, value: null}); + break; + case 'setItem': + memoryStorage.setItem(request.itemName, request.itemValue); + sendResponse({status: true, key: request.itemName, value: request.itemValue}); + break; + } + break; case 'addURLToHistory': chrome.history.addUrl({url: request.url}); break; diff --git a/build/Firefox/lib/main.js b/build/Firefox/lib/main.js index aebfc1a..5311b31 100644 --- a/build/Firefox/lib/main.js +++ b/build/Firefox/lib/main.js @@ -34,15 +34,14 @@ var localStorage = ss.storage; // these aliases are just for simplicity, so that the code here looks just like background code // for all of the other browsers... -localStorage.getItem = function(key) { - return ss.storage[key]; -}; -localStorage.setItem = function(key, value) { - ss.storage[key] = value; -}; -localStorage.removeItem = function(key) { - delete ss.storage[key]; -}; +localStorage. getItem = function(key ) { return ss.storage[key] }; +localStorage. setItem = function(key, value) { ss.storage[key] = value }; +localStorage.removeItem = function(key ) { delete ss.storage[key] }; + +var memoryStorage = { storage: {} }; +memoryStorage. getItem = function(key ) { return memoryStorage.storage[key] }; +memoryStorage. setItem = function(key, value) { memoryStorage.storage[key] = value }; +memoryStorage.removeItem = function(key ) { delete memoryStorage.storage[key] }; var settings = require("./settings.js"); @@ -135,6 +134,38 @@ pageMod.PageMod({ break; } break; + case 'memoryStorage': + switch (request.operation) { + case 'getItem': + worker.postMessage({ + name: 'memoryStorage', + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: memoryStorage.getItem(request.itemName) + }); + break; + case 'removeItem': + memoryStorage.removeItem(request.itemName); + worker.postMessage({ + name: 'memoryStorage', + callbackID: request.callbackID, + status: true, + value: null + }); + break; + case 'setItem': + memoryStorage.setItem(request.itemName, request.itemValue); + worker.postMessage({ + name: 'memoryStorage', + callbackID: request.callbackID, + status: true, + key: request.itemName, + value: request.itemValue + }); + break; + } + break; case 'preferences': switch (request.operation) { case 'getItem': diff --git a/build/Safari.safariextension/background.html b/build/Safari.safariextension/background.html index d06f84f..e29ae03 100644 --- a/build/Safari.safariextension/background.html +++ b/build/Safari.safariextension/background.html @@ -1,6 +1,11 @@