From c7aeffc03a510677c2a59df547c36ee6f4fe66fa Mon Sep 17 00:00:00 2001 From: marta Date: Fri, 31 Oct 2014 01:10:53 -0700 Subject: [PATCH] Privacy Panel new pull request --- .jshintignore | 1 + apps/privacy-panel/.jshintrc | 11 + apps/privacy-panel/build/build.js | 89 + .../privacy-panel/build/settings.build.jslike | 55 + apps/privacy-panel/index.html | 125 ++ apps/privacy-panel/js/about/main.js | 32 + apps/privacy-panel/js/ala/app_list.js | 77 + apps/privacy-panel/js/ala/blur_slider.js | 138 ++ .../js/ala/define_custom_location.js | 273 +++ apps/privacy-panel/js/ala/exception.js | 232 +++ apps/privacy-panel/js/ala/exceptions.js | 150 ++ apps/privacy-panel/js/ala/main.js | 211 ++ apps/privacy-panel/js/app.js | 84 + apps/privacy-panel/js/panels.js | 190 ++ apps/privacy-panel/js/root/main.js | 97 + apps/privacy-panel/js/rpp/auth.js | 381 ++++ apps/privacy-panel/js/rpp/main.js | 36 + apps/privacy-panel/js/rpp/passcode.js | 285 +++ apps/privacy-panel/js/rpp/passphrase.js | 117 ++ apps/privacy-panel/js/rpp/screenlock.js | 96 + apps/privacy-panel/js/sms/commands.js | 160 ++ apps/privacy-panel/js/sms/main.js | 227 +++ apps/privacy-panel/js/vendor/alameda.js | 1414 +++++++++++++ .../locales/countries.en-US.properties | 455 +++++ .../locales/privacypanel.en-US.properties | 213 ++ apps/privacy-panel/manifest.webapp | 45 + apps/privacy-panel/resources/about.json | 4 + apps/privacy-panel/resources/countries.json | 1749 +++++++++++++++++ .../style/icons/privacy-panel.png | Bin 0 -> 6560 bytes apps/privacy-panel/style/images/default.png | Bin 0 -> 1343 bytes .../style/images/default@1.5x.png | Bin 0 -> 2023 bytes .../style/images/default@2.25x.png | Bin 0 -> 3082 bytes .../privacy-panel/style/images/default@2x.png | Bin 0 -> 2616 bytes .../style/images/guided_tour/gt-pager.png | Bin 0 -> 1182 bytes .../guided_tour/privacy-panel-gt-01.svg | 345 ++++ .../guided_tour/privacy-panel-gt-02.svg | 493 +++++ .../guided_tour/privacy-panel-gt-03.svg | 271 +++ .../guided_tour/privacy-panel-gt-04.svg | 535 +++++ .../guided_tour/privacy-panel-gt-05.svg | 402 ++++ .../guided_tour/privacy-panel-gt-06.svg | 463 +++++ .../guided_tour/privacy-panel-gt-07.svg | 363 ++++ .../guided_tour/privacy-panel-gt-08.svg | 355 ++++ .../guided_tour/privacy-panel-gt-09.svg | 493 +++++ .../guided_tour/privacy-panel-gt-10.svg | 223 +++ .../style/images/range_thumb.png | Bin 0 -> 636 bytes .../style/images/range_thumb@1.5x.png | Bin 0 -> 960 bytes .../style/images/range_thumb@2.25x.png | Bin 0 -> 1389 bytes .../style/images/range_thumb@2x.png | Bin 0 -> 996 bytes apps/privacy-panel/style/lists.css | 435 ++++ apps/privacy-panel/style/main.css | 195 ++ apps/privacy-panel/style/menu.css | 76 + apps/privacy-panel/style/panels.css | 588 ++++++ apps/privacy-panel/templates/about/main.html | 34 + apps/privacy-panel/templates/ala/custom.html | 53 + .../templates/ala/exception.html | 61 + .../templates/ala/exceptions.html | 19 + apps/privacy-panel/templates/ala/main.html | 74 + apps/privacy-panel/templates/gt/ala_blur.html | 29 + .../templates/gt/ala_custom.html | 29 + .../templates/gt/ala_exceptions.html | 29 + .../templates/gt/ala_explain.html | 29 + apps/privacy-panel/templates/gt/main.html | 28 + .../templates/gt/rpp_explain.html | 29 + .../templates/gt/rpp_locate.html | 31 + apps/privacy-panel/templates/gt/rpp_lock.html | 31 + .../templates/gt/rpp_passphrase.html | 29 + apps/privacy-panel/templates/gt/rpp_ring.html | 31 + .../templates/rpp/change_pass.html | 30 + .../privacy-panel/templates/rpp/features.html | 42 + apps/privacy-panel/templates/rpp/main.html | 42 + .../privacy-panel/templates/rpp/passcode.html | 39 + .../templates/rpp/screenlock.html | 44 + apps/privacy-panel/test/marionette/.jshintrc | 19 + .../test/marionette/ala_main_test.js | 95 + .../test/marionette/ftu_guided_tour_test.js | 20 + .../test/marionette/guided_tour_test.js | 61 + .../privacy-panel/test/marionette/lib/base.js | 87 + .../test/marionette/lib/panels/ala_main.js | 26 + .../marionette/lib/panels/ftu_guided_tour.js | 38 + .../test/marionette/lib/panels/guided_tour.js | 62 + .../test/marionette/lib/panels/root.js | 53 + .../marionette/lib/panels/rpp_features.js | 106 + .../test/marionette/lib/panels/rpp_main.js | 66 + .../marionette/lib/panels/settings_app.js | 32 + .../test/marionette/manifest.ini | 3 + .../test/marionette/root_test.js | 38 + .../test/marionette/rpp_features_test.js | 57 + .../test/marionette/rpp_main_test.js | 46 + .../test/marionette/running_app_test.js | 38 + apps/privacy-panel/test/unit/.jshintrc | 18 + .../test/unit/ala/app_list_test.js | 110 ++ .../test/unit/ala/blur_slider_test.js | 49 + .../unit/ala/define_custom_location_test.js | 161 ++ apps/privacy-panel/test/unit/html_helper.js | 57 + .../test/unit/mocks/mock_async_storage.js | 18 + .../test/unit/mocks/mock_commands.js | 78 + .../test/unit/mocks/mock_passphrase.js | 54 + apps/privacy-panel/test/unit/rpp/auth_test.js | 128 ++ .../test/unit/rpp/passphrase_test.js | 45 + apps/privacy-panel/test/unit/setup.js | 43 + apps/privacy-panel/test/unit/sms/main_test.js | 193 ++ apps/settings/elements/root.html | 8 +- apps/settings/js/panels/root/panel.js | 8 + .../js/panels/root/privacy_panel_item.js | 116 ++ .../locales/settings.en-US.properties | 4 + .../panels/root/privacy_panel_item_test.js | 71 + build/config/phone/apps-production.list | 1 + 107 files changed, 14925 insertions(+), 1 deletion(-) create mode 100644 apps/privacy-panel/.jshintrc create mode 100644 apps/privacy-panel/build/build.js create mode 100644 apps/privacy-panel/build/settings.build.jslike create mode 100644 apps/privacy-panel/index.html create mode 100644 apps/privacy-panel/js/about/main.js create mode 100644 apps/privacy-panel/js/ala/app_list.js create mode 100644 apps/privacy-panel/js/ala/blur_slider.js create mode 100644 apps/privacy-panel/js/ala/define_custom_location.js create mode 100644 apps/privacy-panel/js/ala/exception.js create mode 100644 apps/privacy-panel/js/ala/exceptions.js create mode 100644 apps/privacy-panel/js/ala/main.js create mode 100644 apps/privacy-panel/js/app.js create mode 100644 apps/privacy-panel/js/panels.js create mode 100644 apps/privacy-panel/js/root/main.js create mode 100644 apps/privacy-panel/js/rpp/auth.js create mode 100644 apps/privacy-panel/js/rpp/main.js create mode 100644 apps/privacy-panel/js/rpp/passcode.js create mode 100644 apps/privacy-panel/js/rpp/passphrase.js create mode 100644 apps/privacy-panel/js/rpp/screenlock.js create mode 100644 apps/privacy-panel/js/sms/commands.js create mode 100644 apps/privacy-panel/js/sms/main.js create mode 100644 apps/privacy-panel/js/vendor/alameda.js create mode 100644 apps/privacy-panel/locales/countries.en-US.properties create mode 100644 apps/privacy-panel/locales/privacypanel.en-US.properties create mode 100644 apps/privacy-panel/manifest.webapp create mode 100644 apps/privacy-panel/resources/about.json create mode 100644 apps/privacy-panel/resources/countries.json create mode 100644 apps/privacy-panel/style/icons/privacy-panel.png create mode 100644 apps/privacy-panel/style/images/default.png create mode 100644 apps/privacy-panel/style/images/default@1.5x.png create mode 100644 apps/privacy-panel/style/images/default@2.25x.png create mode 100644 apps/privacy-panel/style/images/default@2x.png create mode 100755 apps/privacy-panel/style/images/guided_tour/gt-pager.png create mode 100644 apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-01.svg create mode 100644 apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-02.svg create mode 100644 apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-03.svg create mode 100644 apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-04.svg create mode 100644 apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-05.svg create mode 100644 apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-06.svg create mode 100644 apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-07.svg create mode 100644 apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-08.svg create mode 100644 apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-09.svg create mode 100644 apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-10.svg create mode 100644 apps/privacy-panel/style/images/range_thumb.png create mode 100644 apps/privacy-panel/style/images/range_thumb@1.5x.png create mode 100644 apps/privacy-panel/style/images/range_thumb@2.25x.png create mode 100644 apps/privacy-panel/style/images/range_thumb@2x.png create mode 100644 apps/privacy-panel/style/lists.css create mode 100644 apps/privacy-panel/style/main.css create mode 100644 apps/privacy-panel/style/menu.css create mode 100644 apps/privacy-panel/style/panels.css create mode 100644 apps/privacy-panel/templates/about/main.html create mode 100644 apps/privacy-panel/templates/ala/custom.html create mode 100644 apps/privacy-panel/templates/ala/exception.html create mode 100644 apps/privacy-panel/templates/ala/exceptions.html create mode 100644 apps/privacy-panel/templates/ala/main.html create mode 100644 apps/privacy-panel/templates/gt/ala_blur.html create mode 100644 apps/privacy-panel/templates/gt/ala_custom.html create mode 100644 apps/privacy-panel/templates/gt/ala_exceptions.html create mode 100644 apps/privacy-panel/templates/gt/ala_explain.html create mode 100644 apps/privacy-panel/templates/gt/main.html create mode 100644 apps/privacy-panel/templates/gt/rpp_explain.html create mode 100644 apps/privacy-panel/templates/gt/rpp_locate.html create mode 100644 apps/privacy-panel/templates/gt/rpp_lock.html create mode 100644 apps/privacy-panel/templates/gt/rpp_passphrase.html create mode 100644 apps/privacy-panel/templates/gt/rpp_ring.html create mode 100644 apps/privacy-panel/templates/rpp/change_pass.html create mode 100644 apps/privacy-panel/templates/rpp/features.html create mode 100644 apps/privacy-panel/templates/rpp/main.html create mode 100644 apps/privacy-panel/templates/rpp/passcode.html create mode 100644 apps/privacy-panel/templates/rpp/screenlock.html create mode 100644 apps/privacy-panel/test/marionette/.jshintrc create mode 100644 apps/privacy-panel/test/marionette/ala_main_test.js create mode 100644 apps/privacy-panel/test/marionette/ftu_guided_tour_test.js create mode 100644 apps/privacy-panel/test/marionette/guided_tour_test.js create mode 100644 apps/privacy-panel/test/marionette/lib/base.js create mode 100644 apps/privacy-panel/test/marionette/lib/panels/ala_main.js create mode 100644 apps/privacy-panel/test/marionette/lib/panels/ftu_guided_tour.js create mode 100644 apps/privacy-panel/test/marionette/lib/panels/guided_tour.js create mode 100644 apps/privacy-panel/test/marionette/lib/panels/root.js create mode 100644 apps/privacy-panel/test/marionette/lib/panels/rpp_features.js create mode 100644 apps/privacy-panel/test/marionette/lib/panels/rpp_main.js create mode 100644 apps/privacy-panel/test/marionette/lib/panels/settings_app.js create mode 100644 apps/privacy-panel/test/marionette/manifest.ini create mode 100644 apps/privacy-panel/test/marionette/root_test.js create mode 100644 apps/privacy-panel/test/marionette/rpp_features_test.js create mode 100644 apps/privacy-panel/test/marionette/rpp_main_test.js create mode 100644 apps/privacy-panel/test/marionette/running_app_test.js create mode 100644 apps/privacy-panel/test/unit/.jshintrc create mode 100644 apps/privacy-panel/test/unit/ala/app_list_test.js create mode 100644 apps/privacy-panel/test/unit/ala/blur_slider_test.js create mode 100644 apps/privacy-panel/test/unit/ala/define_custom_location_test.js create mode 100644 apps/privacy-panel/test/unit/html_helper.js create mode 100644 apps/privacy-panel/test/unit/mocks/mock_async_storage.js create mode 100644 apps/privacy-panel/test/unit/mocks/mock_commands.js create mode 100644 apps/privacy-panel/test/unit/mocks/mock_passphrase.js create mode 100644 apps/privacy-panel/test/unit/rpp/auth_test.js create mode 100644 apps/privacy-panel/test/unit/rpp/passphrase_test.js create mode 100644 apps/privacy-panel/test/unit/setup.js create mode 100644 apps/privacy-panel/test/unit/sms/main_test.js create mode 100644 apps/settings/js/panels/root/privacy_panel_item.js create mode 100644 apps/settings/test/unit/panels/root/privacy_panel_item_test.js diff --git a/.jshintignore b/.jshintignore index c0247fb0634c..85ff90b8f069 100644 --- a/.jshintignore +++ b/.jshintignore @@ -7,6 +7,7 @@ apps/calendar/build/** apps/calendar/js/ext/** apps/camera/js/vendor/** apps/camera/test/** +apps/privacy-panel/js/vendor/** apps/system/camera/** build/r.js build/csslinter.js diff --git a/apps/privacy-panel/.jshintrc b/apps/privacy-panel/.jshintrc new file mode 100644 index 000000000000..f9abdafb7044 --- /dev/null +++ b/apps/privacy-panel/.jshintrc @@ -0,0 +1,11 @@ +{ + "extends": "../../.jshintrc", + "predef": [ + "define", + "require", + "Promise", + "crypto", + "TextEncoder", + "TextDecoder" + ] +} diff --git a/apps/privacy-panel/build/build.js b/apps/privacy-panel/build/build.js new file mode 100644 index 000000000000..6fb9792fe83a --- /dev/null +++ b/apps/privacy-panel/build/build.js @@ -0,0 +1,89 @@ +'use strict'; + +/* global require, exports, dump */ +var utils = require('utils'); + +function hasGitCommand() { + return utils.getEnvPath().some(function(path) { + try { + var cmd = utils.getFile(path, 'git'); + return cmd.exists(); + } catch (e) { + // path not found + } + return false; + }); +} + +var PrivacyPanelAppBuilder = function() {}; + +PrivacyPanelAppBuilder.prototype.readVersionFile = function(options) { + var aboutFile = utils.getFile(options.STAGE_APP_DIR, '/resources/about.json'); + var aboutFileContent = utils.getFileContent(aboutFile); + return aboutFileContent; +}; + +PrivacyPanelAppBuilder.prototype.getLastCommit = function(options, callback) { + var gitDir = utils.getFile(options.GAIA_DIR, '.git'); + if (gitDir.exists() && hasGitCommand()) { + var git = new utils.Commander('git'); + var stderr, stdout; + var args = [ + '--git-dir=' + gitDir.path, + 'log', + '--format=%H', + 'HEAD', + '-1' + ]; + + var cmdOptions = { + stdout: function(data) { + stdout = data; + }, + stderr: function(data) { + stderr = data; + }, + done: function(data) { + if (data.exitCode === 0) { + utils.log('privacy-panel-app-build', 'Last commit: ' + stdout); + callback(stdout); + } + } + }; + + git.initPath(utils.getEnvPath()); + git.runWithSubprocess(args, cmdOptions); + } +}; + +PrivacyPanelAppBuilder.prototype.executeRjs = function(options) { + var optimize = 'optimize=' + + (options.GAIA_OPTIMIZE === '1' ? 'uglify2' : 'none'); + var configFile = utils.getFile(options.APP_DIR, 'build', + 'settings.build.jslike'); + var r = require('r-wrapper').get(options.GAIA_DIR); + r.optimize([configFile.path, optimize], function() { + dump('require.js optimize ok\n'); + }, function(err) { + dump('require.js optmize failed:\n'); + dump(err + '\n'); + }); +}; + +PrivacyPanelAppBuilder.prototype.execute = function(options) { + this.executeRjs(options); + + this.getLastCommit(options, function(commit) { + var aboutFile = utils + .getFile(options.STAGE_APP_DIR, '/resources/about.json'); + var aboutContent = utils + .readJSONFromPath(options.STAGE_APP_DIR + '/resources/about.json'); + + aboutContent.build = commit.substring(0, 10); + utils.writeContent(aboutFile, JSON.stringify(aboutContent)); + }); +}; + +exports.execute = function(options) { + (new PrivacyPanelAppBuilder()).execute(options); +}; diff --git a/apps/privacy-panel/build/settings.build.jslike b/apps/privacy-panel/build/settings.build.jslike new file mode 100644 index 000000000000..a0a8f161b828 --- /dev/null +++ b/apps/privacy-panel/build/settings.build.jslike @@ -0,0 +1,55 @@ +{ + appDir: '..', + baseUrl: 'js', + mainConfigFile: '../js/app.js', + dir: '../../../build_stage/privacy-panel', + + // Set the path to "empty" to prevent the scripts defining global objects + // from being merged or they are removed after the optimization process, + // which makes the objects inaccessible by reference. + // If the inquiries to the object are all performed by requirejs, we can + // remove the path of the object from the following list. + paths: { + 'shared/l10n': 'empty:', + 'shared/lazy_loader': 'empty:', + 'shared/settings_listener': 'empty:', + 'shared/settings_url': 'empty:', + 'shared/settings_helper': 'empty:', + 'shared/async_storage': 'empty:' + }, + + findNestedDependencies: true, + + // Be sure to normalize all define() calls by extracting + // dependencies so Function toString is not needed, and + // lower capability devices like Tarako can optimize + // memory by discarding function sources. This is + // automatically done when an 'optimize' value other than + // 'none' is used. This setting makes sure it happens for + // builds where 'none' is used for 'optimize'. + normalizeDirDefines: 'all', + + // optimize is now passed via Makefile's GAIA_SETTINGS_MINIFY + // default is none if not passed at all. + // optimize: 'none', + + // Just strip comments, no code compression or mangling. + // Only active if optimize: 'uglify2' + uglify2: { + // Comment out the output section to get rid of line + // returns and tabs spacing. + output: { + beautify: false + }, + compress: true, + mangle: true + }, + + fileExclusionRegExp: /^\.|^test$|^build$/, + + // Keeping build dir since Makefile cleans it up and + // preps build dir with the shared directory + keepBuildDir: true, + removeCombined: true, + modules: [{ name: 'app' }] +} diff --git a/apps/privacy-panel/index.html b/apps/privacy-panel/index.html new file mode 100644 index 000000000000..e7008168baf6 --- /dev/null +++ b/apps/privacy-panel/index.html @@ -0,0 +1,125 @@ + + + + + + + Privacy Panel + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ back + + about + +

Privacy Panel

+
+ +
+ + +
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ + +
+ + + + diff --git a/apps/privacy-panel/js/about/main.js b/apps/privacy-panel/js/about/main.js new file mode 100644 index 000000000000..c8776cc64a04 --- /dev/null +++ b/apps/privacy-panel/js/about/main.js @@ -0,0 +1,32 @@ +/** + * About page panel + * + * @module About + * @return {Object} + */ +define([ + 'panels' +], + +function(panels) { + 'use strict'; + + var About = { + + init: function() { + this.panel = document.getElementById('about'); + this.version = this.panel.querySelector('#privacy-panel-version'); + this.build = this.panel.querySelector('#privacy-panel-build'); + + panels.loadJSON('resources/about.json', function(data) { + this.regionsAndCities = data; + + this.version.textContent = data.version; + this.build.textContent = data.build; + }.bind(this)); + } + }; + + return About; + +}); diff --git a/apps/privacy-panel/js/ala/app_list.js b/apps/privacy-panel/js/ala/app_list.js new file mode 100644 index 000000000000..0d29ea82df98 --- /dev/null +++ b/apps/privacy-panel/js/ala/app_list.js @@ -0,0 +1,77 @@ +/** + * ALA app list module. + * + * @module AppList + * @return {Object} + */ +define([], + +function() { + 'use strict'; + + function AppList() { + this.mozApps = navigator.mozApps; + } + + AppList.prototype = { + + get: function(filter, callback) { + var list = []; + + callback = callback || function() {}; + this.mozApps.mgmt.getAll().onsuccess = function(event) { + var apps = event.target.result; + apps.forEach(function(app) { + var manifest = app.manifest || app.updateManifest; + if (manifest.permissions && manifest.permissions[filter]) { + list.push(app); + } + }); + callback(list); + }; + }, + + icon: function(app) { + var manifest = app.manifest || app.updateManifest; + + if (!manifest.icons || !Object.keys(manifest.icons).length) { + return '../style/images/default.png'; + } + + // The preferred size is 30 by the default. If we use HDPI device, we may + // use the image larger than 30 * 1.5 = 45 pixels. + var preferredIconSize = 30 * (window.devicePixelRatio || 1); + var preferredSize = Number.MAX_VALUE; + var max = 0; + + for (var size in manifest.icons) { + if (manifest.icons.hasOwnProperty(size)) { + size = parseInt(size, 10); + if (size > max) { + max = size; + } + + if (size >= preferredIconSize && size < preferredSize) { + preferredSize = size; + } + } + } + // If there is an icon matching the preferred size, we return the result, + // if there isn't, we will return the maximum available size. + if (preferredSize === Number.MAX_VALUE) { + preferredSize = max; + } + + var url = manifest.icons[preferredSize]; + + if (url) { + return !(/^(http|https|data):/.test(url)) ? app.origin + url : url; + } else { + return '../style/images/default.png'; + } + } + }; + + return new AppList(); + +}); diff --git a/apps/privacy-panel/js/ala/blur_slider.js b/apps/privacy-panel/js/ala/blur_slider.js new file mode 100644 index 000000000000..0958745be970 --- /dev/null +++ b/apps/privacy-panel/js/ala/blur_slider.js @@ -0,0 +1,138 @@ +/** + * ALA blur slider module. + * + * @module BlurSlider + * @return {Object} + */ +define([], + +function() { + 'use strict'; + + function BlurSlider() {} + + BlurSlider.prototype = { + /** + * Initialize ala blur slider. + * @param {Object} element + * @param {String} value + * @param {Function} callback + * @return {BlurSlider} + */ + init: function(element, value, callback) { + this.callback = callback || function(){}; + + this.input = element.querySelector('.blur-slider'); + this.label = element.querySelector('.blur-label'); + + this._setLabel(value); + + this.events(); + + return this; + }, + + /** + * Register events. + */ + events: function() { + this.input.addEventListener('touchmove', function(event) { + this._setLabel(event.target.value); + }.bind(this)); + + this.input.addEventListener('change', function(event) { + this._changeSliderValue(event.target.value); + }.bind(this)); + }, + + /** + * Get input value. + * @return {String} + */ + getValue: function() { + return this.input.value; + }, + + /** + * Set input value. + * @param {String} value + */ + setValue: function(value) { + this.input.value = value; + this._setLabel(value); + }, + + /** + * Change slider value. + * @param {String} value + */ + _changeSliderValue: function(value) { + + // value validation + value = (value > 0 && value <= 12) ? value : 1; + + // update label + this._setLabel(value); + + // run callback + this.callback(this.getRadius(value)); + }, + + /** + * Set radius label. + * @param {String} value + */ + _setLabel: function(value) { + this.label.textContent = BlurSlider.getLabel(value); + }, + + /** + * Get radius value from input value. + * @param {Number} value + * @return {Number} + */ + getRadius: function(value) { + switch(parseInt(value)) { + case 1: return 0.5; + case 2: return 1; + case 3: return 2; + case 4: return 5; + case 5: return 10; + case 6: return 15; + case 7: return 20; + case 8: return 50; + case 9: return 75; + case 10: return 100; + case 11: return 500; + case 12: return 1000; + default: return null; + } + } + }; + + /** + * Get radius label from input value. + * @param {Number} value + * @return {String} + */ + BlurSlider.getLabel = function(value) { + switch(parseInt(value)) { + case 1: return '500m'; + case 2: return '1km'; + case 3: return '2km'; + case 4: return '5km'; + case 5: return '10km'; + case 6: return '15km'; + case 7: return '20km'; + case 8: return '50km'; + case 9: return '75km'; + case 10: return '100km'; + case 11: return '500km'; + case 12: return '1000km'; + default: return ''; + } + }; + + return BlurSlider; + +}); diff --git a/apps/privacy-panel/js/ala/define_custom_location.js b/apps/privacy-panel/js/ala/define_custom_location.js new file mode 100644 index 000000000000..eb7638920162 --- /dev/null +++ b/apps/privacy-panel/js/ala/define_custom_location.js @@ -0,0 +1,273 @@ +/** + * ALA define custom location panel. + * + * @module ALADefineCustomLocation + * @return {Object} + */ +define([ + 'panels', + 'shared/settings_listener', + 'shared/settings_helper' +], + +function(panels, SettingsListener, SettingsHelper) { + 'use strict'; + + function ALADefineCustomLocation() { + this.timeZone = null; + this.context = null; + + this.listeners = { + typeChange: this.toggleType.bind(this), + regionChange: this.toggleRegion.bind(this), + cityChange: this.toggleCity.bind(this), + longitudeChange: this.toggleLongitude.bind(this), + latitudeChange: this.toggleLatitude.bind(this) + }; + + this.selectedRegionCities = {}; + this.config = {}; + } + + ALADefineCustomLocation.prototype = { + + /** + * Initialize ALA Define Custom Location panel. + * + * @method init + * @constructor + */ + init: function() { + this.panel = document.getElementById('ala-custom'); + this.typeRC = this.panel.querySelector('.dcl-type-rc'); + this.typeGPS = this.panel.querySelector('.dcl-type-gps'); + this.regions = this.panel.querySelector('.dcl-region'); + this.cities = this.panel.querySelector('.dcl-city'); + this.longitude = this.panel.querySelector('.dcl-longitude'); + this.latitude = this.panel.querySelector('.dcl-latitude'); + + panels.loadJSON('resources/countries.json', function(data) { + this.regionsAndCities = data; + }.bind(this)); + + this.observers(); + this.events(); + }, + + /** + * Settings observers + */ + observers: function() { + SettingsListener.observe('time.timezone.user-selected', '', + function(value) { + this.timeZone = { + region: value.replace(/\/.*/, '').toLowerCase(), + city: value.replace(/.*?\//, '').toLowerCase() + }; + }.bind(this)); + }, + + /** + * Register events. + */ + events: function() { + this.typeRC.addEventListener('change', this.listeners.typeChange); + this.typeGPS.addEventListener('change', this.listeners.typeChange); + + this.regions.addEventListener('change', this.listeners.regionChange); + this.cities.addEventListener('change', this.listeners.cityChange); + + this.longitude.addEventListener('change', this.listeners.longitudeChange); + this.latitude.addEventListener('change', this.listeners.latitudeChange); + + this.panel.addEventListener('pagerendered', this.onBeforeShow.bind(this)); + + this.panel.querySelector('.back').addEventListener('click', + function() { + this.context.goBackFromDCL(); + }.bind(this) + ); + }, + + /** + * Actions before displaying panel. + * @param event + */ + onBeforeShow: function(event) { + this.context = event.detail || null; + + this.config = this.context.getDCLData(); + this.config.type = this.config.type || 'rc'; + + this.callback = + this.context.saveDCLData.bind(this.context) || function(){}; + + this.updateRegionsList(); + this.updateType(); + + this.saveConfig(); + }, + + toggleType: function(event) { + this.config.type = event.target.value; + this.updateType(); + this.saveConfig(); + }, + + toggleRegion: function(event) { + this.config.region = event.target.value; + this.updateRegion(); + this.updateLongitudeAndLatitudeForCity(); + this.saveConfig(); + }, + + toggleCity: function(event) { + this.config.city = event.target.value; + this.updateCity(); + this.updateLongitudeAndLatitudeForCity(); + this.saveConfig(); + }, + + toggleLongitude: function(event) { + this.config.longitude = event.target.value; + this.saveConfig(); + }, + + toggleLatitude: function(event) { + this.config.latitude = event.target.value; + this.saveConfig(); + }, + + updateRegionsList: function() { + // set new list of cities for selected region + this.selectedRegionCities = this.regionsAndCities[this.config.region]; + + var options = document.createDocumentFragment(); + Object.keys(this.regionsAndCities).forEach(function(regionName) { + var option = document.createElement('option'); + option.value = regionName; + option.setAttribute('data-l10n-id', regionName); + options.appendChild(option); + }.bind(this)); + + // prepare new regions list + this.regions.innerHTML = ''; + this.regions.appendChild(options); + }, + + updateType: function() { + // gps will be enabled by default + this.config.type = this.config.type || 'gps'; + + this.panel.dataset.type = this.config.type; + + this.updateRegion(); + + var modeRC = (this.config.type === 'rc'); + + if (modeRC) { + this.updateLongitudeAndLatitudeForCity(); + } else { + this.updateLongitudeAndLatitude(); + } + + this.typeRC.checked = modeRC; + this.longitude.disabled = modeRC; + this.latitude.disabled = modeRC; + + this.typeGPS.checked = !modeRC; + this.regions.disabled = !modeRC; + this.cities.disabled = !modeRC; + }, + + updateRegion: function() { + if (!this.regionsAndCities[this.config.region] || + this.config.region === undefined) { + this.config.region = + (this.timeZone && + this.regionsAndCities[this.timeZone.region]) ? + this.timeZone.region : + this.getFirstRegion(); + } + + this.regions.value = this.config.region; + + this.updateCitiesList(); + this.updateCity(); + }, + + getFirstRegion: function() { + return Object.keys(this.regionsAndCities)[0] || null; + }, + + updateCitiesList: function() { + this.selectedRegionCities = this.regionsAndCities[this.config.region]; + + var options = document.createDocumentFragment(); + + Object.keys(this.selectedRegionCities).forEach(function(cityName) { + var option = document.createElement('option'); + option.value = cityName; + option.setAttribute('data-l10n-id', cityName); + options.appendChild(option); + }.bind(this)); + + // prepare new cities list + this.cities.innerHTML = ''; + this.cities.appendChild(options); + }, + + updateCity: function() { + if (this.config.city === undefined || + !this.selectedRegionCities[this.config.city]) { + this.config.city = + (this.timeZone && + this.selectedRegionCities[this.timeZone.city]) ? + this.timeZone.city : + this.getFirstCityFromRegion(); + } + + if (this.config.city !== null) { + this.cities.value = this.config.city; + } + }, + + updateLongitudeAndLatitudeForCity: function() { + if (this.config.city !== null) { + var city = this.selectedRegionCities[this.config.city]; + this.config.longitude = city.lon; + this.config.latitude = city.lat; + } else { + this.config.longitude = 0; + this.config.latitude = 0; + } + + this.updateLongitudeAndLatitude(); + }, + + getFirstCityFromRegion: function() { + return Object.keys(this.selectedRegionCities)[0] || null; + }, + + updateLongitudeAndLatitude: function() { + this.longitude.value = this.config.longitude || 0; + this.latitude.value = this.config.latitude || 0; + }, + + validate: function() { + var lat = /^[-+]?(([0-8]\d|\d)(\.\d{1,6})?|90(\.0{1,6})?)$/; + var lon = /^[-+]?((1[0-7]\d(\.\d{1,6})?)|(180(\.0+)?)|(\d\d(\.\d{1,6})?)|(\d(\.\d{1,6})?))$/; //jshint ignore: line + + return lat.test(this.config.latitude) && lon.test(this.config.longitude); + }, + + saveConfig: function() { + if (this.validate()) { + this.callback(this.config); + } + } + }; + + return new ALADefineCustomLocation(); + +}); diff --git a/apps/privacy-panel/js/ala/exception.js b/apps/privacy-panel/js/ala/exception.js new file mode 100644 index 000000000000..e4b948b858f6 --- /dev/null +++ b/apps/privacy-panel/js/ala/exception.js @@ -0,0 +1,232 @@ +/** + * ALA exception panel. + * + * @module ALAException + * @return {Object} + */ +define([ + 'panels', + 'ala/blur_slider', + 'ala/app_list', + 'ala/exceptions', + 'shared/settings_listener', + 'shared/settings_helper' +], + +function(panels, BlurSlider, appList, alaExceptions, SettingsListener, + SettingsHelper) { + 'use strict'; + + function ALAException() { + this.itemData = null; + this.currentApp = null; + this.currentAppSettings = null; + this.blurSlider = new BlurSlider(); + } + + ALAException.prototype = { + + /** + * Initialize ALA exception panel. + * + * @method init + * @constructor + */ + init: function() { + this.panel = document.getElementById('ala-exception'); + + this.appInfoImg = this.panel.querySelector('.app-info img'); + this.appInfoSpan = this.panel.querySelector('.app-info span'); + this.appType = this.panel.querySelector('.app-type'); + this.blurLabel = this.panel.querySelector('.app-blur-label'); + this.alert = this.panel.querySelector('.app-custom-location-alert'); + + this.blurSlider.init( + this.panel.querySelector('.type-blur'), + 1, + this.saveExceptions.bind(this) + ); + + this.events(); + }, + + /** + * Register events. + */ + events: function() { + this.panel.addEventListener('pagerendered', this.onBeforeShow.bind(this)); + + this.appType.addEventListener('change', function(event) { + this.changeAppType(event.target.value, true); + }.bind(this)); + + this.panel.querySelector('.set-custom-location').addEventListener('click', + function() { + panels.show({ id: 'ala-custom', options: this }); + }.bind(this) + ); + + this.alert.querySelector('button').addEventListener('click', + function() { + this.alert.setAttribute('hidden', 'hidden'); + panels.show({ id: 'ala-custom', options: this }); + }.bind(this) + ); + }, + + /** + * Actions before displaying panel. + * @param event + */ + onBeforeShow: function(event) { + this.itemData = event.detail; + + this.appInfoImg.src = this.itemData.iconSrc; + this.appInfoSpan.textContent = this.itemData.name; + + this.currentApp = this.itemData.origin; + this.currentAppSettings = + alaExceptions.exceptionsList[this.itemData.origin]; + + if (!this.currentAppSettings) { + // set default value (from general settings) + this.appType.value = 'system-settings'; + + // change settings type + this.changeAppType('system-settings', false); + } else { + + // set checkbox value + this.appType.value = this.currentAppSettings.type; + + // change settings type + this.changeAppType(this.currentAppSettings.type, false); + + // set slider value + this.blurSlider.setValue(this.currentAppSettings.slider); + } + }, + + /** + * Change Application type. + * @param {String} value + * @param {Boolean} save + */ + changeAppType: function(value, save) { + + // set attribute to section + this.panel.dataset.type = value; + + // hide alert + this.alert.setAttribute('hidden', 'hidden'); + + switch (value) { + case 'user-defined': + /** @TODO: add alert */ + if (!(alaExceptions.exceptionsList[this.currentApp] && + alaExceptions.exceptionsList[this.currentApp].coords)) { + + // show alert if geolocation is not set + this.alert.removeAttribute('hidden'); + } + + break; + case 'system-settings': + // remove application + if (save === true) { + this.removeException(); + } + return; + case 'blur': + case 'precise': + case 'no-location': + break; + default: + break; + } + + // save current type + save && this.saveExceptions(null); + }, + + /** + * Save exception list. + * @param {Object|Null} settings + */ + saveExceptions: function(settings) { + var current = this.currentAppSettings || {}; + var extraSettings = settings || {}; + + alaExceptions.exceptionsList[this.currentApp] = { + type: this.appType.value, + slider: this.blurSlider.getValue(), + radius: this.blurSlider.getRadius(this.blurSlider.getValue()), + + coords: extraSettings.coords || current.coords || null, + cl_type: extraSettings.cl_type || current.cl_type || null, + cl_region: extraSettings.cl_region || current.cl_region || null, + cl_city: extraSettings.cl_city || current.cl_city || null, + cl_longitude: extraSettings.cl_longitude || current.cl_longitude ||null, + cl_latitude: extraSettings.cl_latitude || current.cl_latitude || null + }; + + SettingsHelper('geolocation.app_settings') + .set(alaExceptions.exceptionsList); + }, + + /** + * Remove exception from list. + */ + removeException: function() { + delete alaExceptions.exceptionsList[this.currentApp]; + + SettingsHelper('geolocation.app_settings') + .set(alaExceptions.exceptionsList); + }, + + /** + * Get data for Define Custom Location. + * @return {Array} + */ + getDCLData: function() { + this.currentAppSettings = + alaExceptions.exceptionsList[this.currentApp]; + return { + type: this.currentAppSettings.cl_type, + region: this.currentAppSettings.cl_region, + city: this.currentAppSettings.cl_city, + longitude: this.currentAppSettings.cl_longitude, + latitude: this.currentAppSettings.cl_latitude + }; + }, + + /** + * Save custom location settings. + * @param {Object} settings + */ + saveDCLData: function(settings) { + var flag = settings.latitude !== '' && settings.longitude !== ''; + + this.saveExceptions({ + coords: flag ? '@'+settings.latitude+','+settings.longitude : '', + cl_type: settings.type, + cl_region: settings.region, + cl_city: settings.city, + cl_longitude: settings.longitude, + cl_latitude: settings.latitude + }); + }, + + /** + * Go back from DCL + */ + goBackFromDCL: function() { + panels.show( + { id: 'ala-exception', options: this.itemData, back: true} + ); + } + }; + + return new ALAException(); + +}); diff --git a/apps/privacy-panel/js/ala/exceptions.js b/apps/privacy-panel/js/ala/exceptions.js new file mode 100644 index 000000000000..e9eeec00abc3 --- /dev/null +++ b/apps/privacy-panel/js/ala/exceptions.js @@ -0,0 +1,150 @@ +/** + * ALA exceptions panel. + * + * @module ExceptionsPanel + * @return {Object} + */ +define([ + 'panels', + 'ala/blur_slider', + 'ala/app_list', + 'shared/settings_listener', + 'shared/settings_helper' +], + +function(panels, BlurSlider, appList, SettingsListener, SettingsHelper) { + 'use strict'; + + function ExceptionsPanel() { + this.apps = []; + this.exceptionsList = {}; + } + + ExceptionsPanel.prototype = { + + /** + * Initialize ALA exceptions panel. + * + * @method init + * @constructor + */ + init: function(apps) { + this.panel = document.getElementById('ala-exceptions'); + this.apps = apps; + + this.appListElement = this.panel.querySelector('#app-list'); + + // get exception list from settings + SettingsHelper('geolocation.app_settings', {}).get(function(value){ + this.exceptionsList = value; + }.bind(this)); + + this.events(); + }, + + /** + * Register events. + */ + events: function() { + this.panel.addEventListener('pagerendered', this.onBeforeShow.bind(this)); + }, + + /** + * Actions before displaying panel. + * @param event + */ + onBeforeShow: function(event) { + // remove existing entries from application list + var apps = this.appListElement.querySelectorAll('.app-element'); + for (var el of apps) { + this.appListElement.removeChild(el); + } + + // render app list + var manifest, icon, appSettings, type, li; + + this.apps.forEach(function(item, index) { + + // remove Privacy Panel application from list + if (item.origin.indexOf('privacy-panel') !== -1) { + return; + } + + manifest = item.manifest || item.updateManifest; + icon = appList.icon(item); + + type = undefined; + appSettings = this.exceptionsList[item.origin]; + + if (appSettings) { + type = appSettings.type; + switch (appSettings.type) { + case 'user-defined': + type = 'User defined'; + break; + case 'blur': + type = BlurSlider.getLabel(appSettings.slider) +' blur'; + break; + case 'precise': + type = 'Precise'; + break; + case 'no-location': + type = 'No location'; + break; + default: + break; + } + } + + li = this.renderAppItem({ + origin: item.origin, + name: manifest.name, + index: index, + iconSrc: icon, + type: type + }); + + this.appListElement.appendChild(li); + + }.bind(this)); + }, + + + /** + * Render App item. + * @param itemData + * @returns {HTMLElement} + */ + renderAppItem: function(itemData) { + var icon = document.createElement('img'); + var item = document.createElement('li'); + var link = document.createElement('a'); + var name = document.createElement('span'); + + icon.src = itemData.iconSrc; + name.textContent = itemData.name; + + link.classList.add('menu-item'); + link.appendChild(icon); + link.appendChild(name); + + if (itemData.type) { + var type = document.createElement('small'); + type.textContent = itemData.type; + link.appendChild(type); + } + + link.addEventListener('click', + function() { + panels.show({ id: 'ala-exception', options: itemData }); + }); + + item.classList.add('app-element'); + item.appendChild(link); + return item; + } + }; + + return new ExceptionsPanel(); + +}); diff --git a/apps/privacy-panel/js/ala/main.js b/apps/privacy-panel/js/ala/main.js new file mode 100644 index 000000000000..47f18a1b2b2a --- /dev/null +++ b/apps/privacy-panel/js/ala/main.js @@ -0,0 +1,211 @@ +/** + * ALA main panel. + * + * @module AlaPanel + * @return {Object} + */ +define([ + 'panels', + 'ala/blur_slider', + 'ala/app_list', + 'ala/exception', + 'ala/exceptions', + 'ala/define_custom_location', + 'shared/settings_listener', + 'shared/settings_helper' +], + +function(panels, BlurSlider, appList, alaException, alaExceptions, alaDCL, + SettingsListener, SettingsHelper) { + 'use strict'; + + function AlaPanel() { + this.blurSlider = new BlurSlider(); + this.geolocationCords = null; + this.dclData = {}; + } + + AlaPanel.prototype = { + + /** + * Initialize ala panel. + */ + init: function() { + this.settings = window.navigator.mozSettings; + this.panel = document.getElementById('ala-main'); + this.alert = this.panel.querySelector('.custom-location-alert'); + + //initialize blur slider element + SettingsHelper('geolocation.blur.slider', 1).get(function(value) { + this.blurSlider.init( + this.panel.querySelector('.type-blur'), + value, + function(value) { + SettingsHelper('geolocation.approx_distance').set(value); + }.bind(this) + ); + }.bind(this)); + + this.observers(); + this.events(); + this._prepareDCLData(); + + // prepare app list that uses geolocation + appList.get('geolocation', function(apps) { + + // init alaExceptions module + alaExceptions.init(apps); + }.bind(this)); + + // init alaException module + alaException.init(); + + // init alaDefineCustomLocation module + alaDCL.init(); + }, + + /** + * Settings observers + */ + observers: function() { + SettingsListener.observe('geolocation.fixed_coords', false, + function(value) { + this.geolocationCords = value; + }.bind(this) + ); + + SettingsListener.observe('geolocation.enabled', false, + this.toggleGeolocation.bind(this) + ); + + SettingsListener.observe('ala.settings.enabled', false, + this.toggleALA.bind(this) + ); + + SettingsListener.observe('geolocation.type', false, + this.changeType.bind(this) + ); + }, + + /** + * Register events. + */ + events: function() { + this.panel.querySelector('.set-custom-location').addEventListener('click', + function() { + panels.show({ id: 'ala-custom', options: this }); + }.bind(this) + ); + + this.alert.querySelector('button').addEventListener('click', + function() { + this.alert.setAttribute('hidden', 'hidden'); + panels.show({ id: 'ala-custom', options: this }); + }.bind(this) + ); + }, + + /** + * Toggle Geolocation. + * @param {Boolean} value + */ + toggleGeolocation: function(value) { + this.panel.dataset.geolocation = (value); + }, + + /** + * Toggle Location Accuracy. + * @param {Boolean} value + */ + toggleALA: function(value) { + this.panel.dataset.ala = (value); + }, + + /** + * Change ALA type. + * @param {String} value + */ + changeType: function(value) { + + // set attribute to section + this.panel.dataset.type = value; + + // hide alert + this.alert.setAttribute('hidden', 'hidden'); + + switch (value) { + case 'user-defined': + if (!this.geolocationCords) { + // show alert if geolocation is not set + this.alert.removeAttribute('hidden'); + } + break; + case 'blur': + case 'precise': + case 'no-location': + break; + default: + break; + } + }, + + /** + * Prepare data for Define Custom Location. + */ + _prepareDCLData: function() { + SettingsHelper('geolocation.blur.cl.type').get(function(value){ + this.dclData.type = value; + }.bind(this)); + SettingsHelper('geolocation.blur.cl.region').get(function(value){ + this.dclData.region = value; + }.bind(this)); + SettingsHelper('geolocation.blur.cl.city').get(function(value){ + this.dclData.city = value; + }.bind(this)); + SettingsHelper('geolocation.blur.longitude').get(function(value){ + this.dclData.longitude = value; + }.bind(this)); + SettingsHelper('geolocation.blur.latitude').get(function(value){ + this.dclData.latitude = value; + }.bind(this)); + }, + + /** + * Get data for Define Custom Location. + * @return {Array} + */ + getDCLData: function() { + return Object.create(this.dclData); + }, + + /** + * Save custom location settings. + * @param {Object} settings + */ + saveDCLData: function(settings) { + var flag = settings.latitude !== '' && settings.longitude !== ''; + + this.settings.createLock().set({ + 'geolocation.blur.cl.type': settings.type, + 'geolocation.blur.cl.region': settings.region, + 'geolocation.blur.cl.city': settings.city, + 'geolocation.blur.longitude': settings.longitude, + 'geolocation.blur.latitude': settings.latitude, + 'geolocation.fixed_coords': + flag ? '@' + settings.latitude + ',' + settings.longitude : '' + }); + + this._prepareDCLData(); + }, + + /** + * Go back from DCL + */ + goBackFromDCL: function() { + panels.show({ id: 'ala-main', back: true }); + } + }; + + return new AlaPanel(); + +}); diff --git a/apps/privacy-panel/js/app.js b/apps/privacy-panel/js/app.js new file mode 100644 index 000000000000..04646b036b98 --- /dev/null +++ b/apps/privacy-panel/js/app.js @@ -0,0 +1,84 @@ +'use strict'; + +require.config({ + baseUrl: '/js', + paths: { + 'shared': '../shared/js' + }, + shim: { + 'shared/lazy_loader': { + exports: 'LazyLoader' + }, + 'shared/settings_listener': { + exports: 'SettingsListener' + }, + 'shared/settings_helper': { + exports: 'SettingsHelper' + }, + 'shared/settings_url': { + exports: 'SettingsURL' + }, + 'shared/async_storage': { + exports: 'asyncStorage' + }, + 'shared/l10n': { + exports: 'navigator.mozL10n' + } + } +}); + +(function() { + var ppFTU = navigator.mozSettings.createLock() + .get('privacy-panel-gt-complete'); + ppFTU.onsuccess = function() { + var ftu = ppFTU.result['privacy-panel-gt-complete']; + + if (!ftu) { + var rootPanel = document.getElementById('root'); + rootPanel.classList.remove('current'); + rootPanel.classList.add('previous'); + document.getElementById('gt-main').classList.add('current'); + + navigator.mozSettings.createLock().set({ + 'privacy-panel-gt-complete': true + }); + } + }; +})(); + +require([ + 'panels', + 'root/main', + 'about/main', + 'shared/l10n' +], + +function(panels, root, about) { + root.init(); + + // load all templates for guided tour sections + panels.load('gt'); + panels.load('about', function() { + about.init(); + }); + + require([ + 'ala/main', + 'rpp/main', + 'sms/main' + ], + + function(ala, rpp, commands) { + // load all templates for location accuracy sections + panels.load('ala', function() { + ala.init(); + }); + + // load all templates for remote privacy sections + panels.load('rpp', function() { + rpp.init(); + }); + + commands.init(); + }); +}); diff --git a/apps/privacy-panel/js/panels.js b/apps/privacy-panel/js/panels.js new file mode 100644 index 000000000000..518a887c56a0 --- /dev/null +++ b/apps/privacy-panel/js/panels.js @@ -0,0 +1,190 @@ +/** + * Handles panels. + * + * @module PanelController + * @return {Object} + */ +define([ + 'shared/lazy_loader', + 'shared/settings_listener', + 'shared/settings_helper' +], + +function(lazyLoader, SettingsListener, SettingsHelper) { + 'use strict'; + + function PanelController() {} + + PanelController.prototype = { + + /** + * Load needed templates + * + * @method load + * @param {Array} sections + * @param {Function} callback + */ + load: function(group, callback) { + var result = []; + var sections = document.querySelectorAll( + 'section[data-section="' + group + '"]' + ); + + callback = callback || function() {}; + + // Convert sections to normal array + [].forEach.call(sections, function(section) { + result.push(section); + }); + + lazyLoader.load(result, function() { + this.registerEvents(result); + callback(result); + }.bind(this)); + }, + + /** + * Show specific section, closes previously opened ones. + * + * @method show + * @param {Object} p + * @param {String} p.id [optional] Element ID + * @param {Object} p.el [optional] DOM element + * @param {Boolean} p.back [optional] Trigger back transition + * @param {Mixed} p.options [optional] Passed parameters + */ + show: function(p) { + if (p.id && !p.el) { + p.el = document.getElementById(p.id); + } + _showSection(p.el, p.back, p.options); + }, + + /** + * Change page + * + * @method changePage + * @param {Object} event + */ + changePage: function(event) { + var target, id = this.hash.replace('#', ''); + + event.preventDefault(); + + if (!id) { + return; + } + + target = document.getElementById(id); + _showSection(target, this.classList.contains('back')); + }, + + /** + * Register events for given element + * + * @method registerEvents + * @param sections + */ + registerEvents: function(sections) { + sections.forEach(function(section) { + var links = section.querySelectorAll('.pp-link'); + var settings = section.querySelectorAll('input[name], select[name]'); + + // Redirect each click on pp-links with href attributes + [].forEach.call(links, function(link) { + link.addEventListener('click', this.changePage); + }.bind(this)); + + // Update and save settings on change + [].forEach.call(settings, function(setting) { + SettingsListener.observe( + setting.name, + setting.dataset.default || false, + this.updateSetting.bind(setting) + ); + setting.addEventListener('change', this.saveSetting); + }.bind(this)); + }.bind(this)); + }, + + /** + * JSON loader + * + * @method loadJSON + * @param {String} href + * @param {Function} callback + */ + loadJSON: function(href, callback) { + if (!callback) { + return; + } + + var xhr = new XMLHttpRequest(); + xhr.onerror = function() { + console.error('Failed to fetch file: ' + href, xhr.statusText); + }; + xhr.onload = function() { + callback(xhr.response); + }; + xhr.open('GET', href, true); // async + xhr.responseType = 'json'; + xhr.send(); + }, + + /** + * Update input value + * + * @method updateSetting + * @param {String} value + */ + updateSetting: function(value) { + if (this.type === 'checkbox') { + this.checked = value; + } else { + this.value = value; + } + }, + + /** + * Save input value to mozSettings based on inputs name + * + * @method saveSetting + */ + saveSetting: function() { + var value = this.type === 'checkbox' ? this.checked : this.value; + SettingsHelper(this.name).set(value); + } + }; + + /** + * Show section + * + * @private + * @mrthod showSection + * @param element + * @param {Boolean} back + */ + var _showSection = function(element, back, options) { + var sections = document.querySelectorAll('section'); + var prevClass = back ? '' : 'previous'; + var event = new CustomEvent('pagerendered', { + detail: options, + bubbles: true + }); + + for (var section of sections) { + if (element.id === 'root' && section.className !== '') { + section.className = section.className === 'current' ? prevClass : ''; + } + + if (section.className === 'current') { + section.className = prevClass; + } + } + + element.className = 'current'; + element.dispatchEvent(event); + }; + + return new PanelController(); +}); diff --git a/apps/privacy-panel/js/root/main.js b/apps/privacy-panel/js/root/main.js new file mode 100644 index 000000000000..cdba8af4cef4 --- /dev/null +++ b/apps/privacy-panel/js/root/main.js @@ -0,0 +1,97 @@ +/** + * Root panel. + * + * @module RootPanel + * @return {Object} + */ +define([ + 'panels', + 'shared/settings_listener', + 'shared/settings_helper' +], + +function(panels, SettingsListener, SettingsHelper) { + 'use strict'; + + function RootPanel() {} + + RootPanel.prototype = { + + /** + * Initialize Root panel + * + * @method init + * @constructor + */ + init: function() { + document.querySelector('body').dataset.ready = true; + + this.panel = document.getElementById('root'); + this.backBtn = this.panel.querySelector('#back-to-settings'); + + this.settingsApp = null; + this.settingsManifestURL = document.location.protocol + + '//settings.gaiamobile.org' + (location.port ? (':' + + location.port) : '') + '/manifest.webapp'; + + this.observers(); + this.events(); + }, + + events: function() { + panels.registerEvents([this.panel]); + + // Reset launch flag when app is not active. + window.addEventListener('blur', function() { + SettingsHelper('privacypanel.launched.by.settings').set(false); + }); + + this.backBtn.addEventListener('click', function(event) { + event.preventDefault(); + this.getSettingsApp().then(function(app) { + app.launch(); + }); + }.bind(this)); + }, + + observers: function() { + // Observe 'privacy-panel.launched-by-settings' setting to be able to + // detect launching point. + SettingsListener.observe('privacypanel.launched.by.settings', false, + function(value) { + this.panel.dataset.settings = value; + }.bind(this) + ); + }, + + searchApp: function(appURL, callback) { + navigator.mozApps.mgmt.getAll().onsuccess = function gotApps(evt) { + var app = null, apps = evt.target.result; + for (var i = 0; i < apps.length && app === null; i++) { + if (apps[i].manifestURL === appURL) { + app = apps[i]; + return callback(app); + } + } + }; + }, + + getSettingsApp: function() { + var promise = new Promise(function(resolve) { + if (this.settingsApp) { + resolve(this.settingApp); + } else { + this.searchApp(this.settingsManifestURL, function(app) { + resolve(app); + }); + } + }.bind(this)); + + return promise; + } + + }; + + return new RootPanel(); + +}); diff --git a/apps/privacy-panel/js/rpp/auth.js b/apps/privacy-panel/js/rpp/auth.js new file mode 100644 index 000000000000..7fb0ad78a33e --- /dev/null +++ b/apps/privacy-panel/js/rpp/auth.js @@ -0,0 +1,381 @@ +/** + * Auth panels (login/register/change passphrase). + * + * @module AuthPanel + * @return {Object} + */ +define([ + 'panels', + 'rpp/passphrase', + 'shared/settings_listener' +], + +function(panels, PassPhrase, SettingsListener) { + 'use strict'; + + function AuthPanel() { + this.passphrase; + this.lsPasscode = false; + this.lsPasscodeEnabled = false; + this.simcards = null; + } + + AuthPanel.prototype = { + + /** + * Initialize RPP panel and all its sections + * + * @method init + * @constructor + */ + init: function() { + this.mainPanel = document.getElementById('rpp-main'); + this.changePanel = document.getElementById('rpp-change-pass'); + this.loginForm = document.getElementById('rpp-login-form'); + this.registerForm = document.getElementById('rpp-register-form'); + this.changeForm = document.getElementById('rpp-change-pass-form'); + + this.passphrase = new PassPhrase('rppmac', 'rppsalt'); + + // Define first time use to eventualy show register page + this.defineFTU(); + this.getSIMCards(); + + this.observers(); + this.events(); + }, + + events: function() { + // Submit events + this.loginForm.addEventListener('submit', + this.loginUser.bind(this)); + this.registerForm.addEventListener('submit', + this.registerUser.bind(this)); + this.changeForm.addEventListener('submit', + this.changePassphrase.bind(this)); + + // On show events + this.mainPanel.addEventListener('pagerendered', function() { + this.clearLoginForm(); + this.clearRegisterForm(); + }.bind(this)); + this.changePanel.addEventListener('pagerendered', function() { + this.clearChangeForm(); + }.bind(this)); + + this.changeForm.querySelector('.pin-type').addEventListener('change', + this.onPinTypeChange.bind(this)); + }, + + observers: function() { + SettingsListener.observe('lockscreen.passcode-lock.code', false, + function(value) { + this.lsPasscode = value; + }.bind(this) + ); + + SettingsListener.observe('lockscreen.passcode-lock.enabled', false, + function(value) { + this.lsPasscodeEnabled = value; + + // Each time user decides to disable passcode, show him that he can't + // use rpp features. + this.toggleAlertBox(); + this.fillChangeOptions(); + this.changePanel.querySelector('.pin-type') + .dispatchEvent(new Event('change')); + }.bind(this) + ); + }, + + /** + * Defines whenever we can login to rpp setting or do we need to register + * new passphrase. + * + * @method defineFTU + */ + defineFTU: function() { + this.passphrase.exists().then(function(status) { + this.mainPanel.dataset.loginBox = status; + }.bind(this)); + }, + + /** + * [getSIMStatus description] + * @return {[type]} [description] + */ + getSIMCards: function() { + var mc = navigator.mozMobileConnections; + + if (!mc) { + return; + } + + [].forEach.call(mc, function(connection, key) { + var icc, label; + if (connection.iccId) { + icc = navigator.mozIccManager.getIccById(connection.iccId); + if (icc.cardState === 'ready') { + label = 'SIM ' + (key + 1); + this.simcards = this.simcards ? this.simcards : {}; + this.simcards[label] = icc; + } + } + }.bind(this)); + }, + + fillChangeOptions: function() { + var element, select = this.changePanel.querySelector('.pin-type'); + select.innerHTML = ''; + + for (var simcard in this.simcards) { + if (this.simcards.hasOwnProperty(simcard)) { + element = document.createElement('option'); + element.value = simcard; + element.textContent = simcard; + + var simcardL10n = simcard.toLowerCase().replace(' ', ''); + element.setAttribute('data-l10n-id', simcardL10n); + select.appendChild(element); + } + } + + if (this.lsPasscodeEnabled) { + element = document.createElement('option'); + element.value = 'passcode'; + element.setAttribute('data-l10n-id', 'passcode'); + select.appendChild(element); + } + }, + + onPinTypeChange: function(event) { + var value = event.target.value.toString(); + var input = this.changeForm.querySelector('.pin'); + + value = 'enter-' + value.toLowerCase().replace(' ', ''); + input.setAttribute('data-l10n-id', value); + }, + + /** + * Compares and validates two strings. Returns error strings. + * + * @param {String} pass1 First password + * @param {String} pass2 Second password + * @return {String} Empty string when success + */ + comparePasswords: function(pass1, pass2) { + var rgx = /^([a-z0-9]+)$/i; + + if (!pass1) { + return 'passphrase-empty'; + } + + if (pass1.length > 100) { + return 'passphrase-too-long'; + } + + if (!rgx.test(pass1)) { + return 'passphrase-invalid'; + } + + if (pass1 !== pass2) { + return 'passphrase-different'; + } + + return ''; + }, + + /** + * Compares and validates two strings. Returns error strings. + * + * @param {String} pass1 First password + * @param {String} pass2 Second password + * @return {String} Empty string when success + */ + comparePINs: function(pass1, pass2) { + var rgx = /^([0-9]{1,4})$/i; + + if (!pass1) { + return 'pin-empty'; + } + + if (!rgx.test(pass1)) { + return 'pin-invalid'; + } + + if (pass1 !== pass2) { + return 'pin-different'; + } + + return ''; + }, + + /** + * Register new user so he can use all rpp features. + * + * @method registerUser + * @param {Object} event JavaScript event + */ + registerUser: function(event) { + var form = this.registerForm; + var pass1 = form.querySelector('.pass1').value; + var pass2 = form.querySelector('.pass2').value; + var message = form.querySelector('.validation-message'); + var error; + + event.preventDefault(); + + error = this.comparePasswords(pass1, pass2); + if (error) { + message.setAttribute('data-l10n-id', error); + return; + } + + this.passphrase.change(pass1).then(function() { + panels.show({ id: 'rpp-features' }); + this.defineFTU(); + }.bind(this)); + }, + + /** + * Clear form and validation messages + * + * @method clearRegisterForm + */ + clearRegisterForm: function() { + var form = this.registerForm; + var message = form.querySelector('.validation-message'); + + form.reset(); + message.textContent = ''; + }, + + /** + * Login user to rpp panel + * + * @method loginUser + * @param {Object} event JavaScript event + */ + loginUser: function(event) { + var form = this.loginForm; + var pass = form.querySelector('.pass1').value; + var message = form.querySelector('.validation-message'); + + event.preventDefault(); + + this.passphrase.verify(pass).then(function(status) { + if (!status) { + message.setAttribute('data-l10n-id', 'passphrase-wrong'); + return; + } + + panels.show({ id: 'rpp-features' }); + }.bind(this)); + }, + + /** + * Clear form and validation messages + * + * @method clearLoginForm + */ + clearLoginForm: function() { + var form = this.loginForm; + var message = form.querySelector('.validation-message'); + + form.reset(); + message.textContent = ''; + }, + + /** + * Change passphrase. + * + * @method changePassphrase + * @param {Object} event JavaScript event + */ + changePassphrase: function(event) { + var form = this.changeForm; + var pin = form.querySelector('.pin').value; + var pass1 = form.querySelector('.pass1').value; + var pass2 = form.querySelector('.pass2').value; + var type = form.querySelector('.pin-type').value; + var passmsg = form.querySelector('.validation-message'); + var pinmsg = form.querySelector('.pin-validation-message'); + var passError; + + event.preventDefault(); + + passmsg.textContent = ''; + pinmsg.textContent = ''; + + var resultCallback = function(pinError) { + if (pinError) { + pinmsg.setAttribute('data-l10n-id', pinError); + return; + } + + passError = this.comparePasswords(pass1, pass2); + if (passError) { + passmsg.setAttribute('data-l10n-id', passError); + return; + } + + this.passphrase.change(pass1).then(function() { + panels.show({ id: 'rpp-features' }); + }); + }.bind(this); + + if (type === 'passcode') { + this.verifyPassCode(pin, resultCallback); + } else { + this.verifySIMPIN(this.simcards[type], pin, resultCallback); + } + }, + + verifySIMPIN: function(simcard, pin, callback) { + var unlock = simcard.unlockCardLock({ lockType : 'pin', pin: pin }); + unlock.onsuccess = callback.bind(this, ''); + unlock.onerror = callback.bind(this, 'sim-invalid'); + }, + + verifyPassCode: function(pin, callback) { + var status = this.comparePINs(pin, this.lsPasscode); + callback = callback || function() {}; + + callback(status); + }, + + /** + * Clear form and validation messages + * + * @method clearChangeForm + */ + clearChangeForm: function() { + var form = this.changeForm; + var passmsg = form.querySelector('.validation-message'); + var pinmsg = form.querySelector('.pin-validation-message'); + + form.reset(); + passmsg.textContent = ''; + pinmsg.textContent = ''; + }, + + /** + * Toggle alert box, show it when user doesn't have passcode enabled + * + * @method toggleAlertBox + */ + toggleAlertBox: function() { + var modal = document.querySelector('#rpp-features .overlay'); + + if (this.lsPasscodeEnabled) { + modal.setAttribute('hidden', 'hidden'); + } else { + modal.removeAttribute('hidden'); + } + } + + }; + + return new AuthPanel(); + +}); diff --git a/apps/privacy-panel/js/rpp/main.js b/apps/privacy-panel/js/rpp/main.js new file mode 100644 index 000000000000..8ac68ea960b3 --- /dev/null +++ b/apps/privacy-panel/js/rpp/main.js @@ -0,0 +1,36 @@ +/** + * Remote Privacy Protection panel. + * + * @module RppPanel + * @return {Object} + */ +define([ + 'rpp/auth', + 'rpp/screenlock', + 'rpp/passcode', +], + +function(rppAuth, rppScreenLock, rppPassCode) { + 'use strict'; + + function RppPanel() {} + + RppPanel.prototype = { + + /** + * Initialize RPP panel and all its sections + * + * @method init + * @constructor + */ + init: function() { + rppAuth.init(); + rppScreenLock.init(); + rppPassCode.init(); + } + + }; + + return new RppPanel(); + +}); diff --git a/apps/privacy-panel/js/rpp/passcode.js b/apps/privacy-panel/js/rpp/passcode.js new file mode 100644 index 000000000000..f4059f2f6ab1 --- /dev/null +++ b/apps/privacy-panel/js/rpp/passcode.js @@ -0,0 +1,285 @@ +/** + * PassCode panel. + * + * @module PassCodePanel + * @return {Object} + */ +define([ + 'panels', + 'shared/settings_listener', +], + +function(panels, SettingsListener) { + 'use strict'; + + function PassCodePanel() {} + + PassCodePanel.prototype = { + + panel: null, + + /** + * create : when the user turns on passcode settings + * edit : when the user presses edit passcode button + * confirm : when the user turns off passcode settings + * new : when the user is editing passcode + * and has entered old passcode successfully + */ + _MODE: 'create', + + _settings: { + passcode: '0000' + }, + + _checkingLength: { + 'create': 8, + 'new': 8, + 'edit': 4, + 'confirm': 4, + 'confirmLock': 4 + }, + + _passcodeBuffer: '', + + init: function() { + this.panel = document.getElementById('rpp-passcode'); + this._getAllElements(); + this.passcodeInput.addEventListener('keypress', this); + this.createPasscodeButton.addEventListener('click', this); + this.changePasscodeButton.addEventListener('click', this); + + // If the pseudo-input loses focus, then allow the user to restore focus + // by touching the container around the pseudo-input. + this.passcodeContainer.addEventListener('click', function(evt) { + this.passcodeInput.focus(); + evt.preventDefault(); + }.bind(this)); + + this._fetchSettings(); + + this.panel.addEventListener('pagerendered', + this.onBeforeShow.bind(this)); + }, + + /** + * Re-runs the font-fit title + * centering logic. + * + * The gaia-header has mutation observers + * that listen for changes in the header + * title and re-run the font-fit logic. + * + * If buttons around the title are shown/hidden + * then these mutation observers won't be + * triggered, but we want the font-fit logic + * to be re-run. + * + * This is a deficiency of . If + * anyone knows a way to listen for changes + * in visibility, we won't need this anymore. + * + * @param {GaiaHeader} header + * @private + */ + runHeaderFontFit: function su_runHeaderFontFit(header) { + var titles = header.querySelectorAll('h1'); + [].forEach.call(titles, function(title) { + title.textContent = title.textContent; + }); + }, + + _getAllElements: function sld_getAllElements() { + this.passcodePanel = this.panel; + this.header = this.panel.querySelector('header'); + this.passcodeInput = this.panel.querySelector('.passcode-input'); + this.passcodeDigits = this.panel.querySelectorAll('.passcode-digit'); + this.passcodeContainer = + this.panel.querySelector('.passcode-container'); + this.createPasscodeButton = + this.panel.querySelector('.passcode-create'); + this.changePasscodeButton = + this.panel.querySelector('.passcode-change'); + }, + + onBeforeShow: function sld_onBeforeShow(event) { + this._showDialogInMode(event.detail || 'create'); + setTimeout(this.onShow.bind(this), 100); + }, + + onShow: function sld_onShow() { + this.passcodeInput.focus(); + }, + + _showDialogInMode: function sld_showDialogInMode(mode) { + this._hideErrorMessage(); + this._MODE = mode; + this.passcodePanel.dataset.mode = mode; + this._updatePassCodeUI(); + this.runHeaderFontFit(this.header); + }, + + handleEvent: function sld_handleEvent(evt) { + var settings; + var passcode; + var lock; + + switch (evt.target) { + case this.passcodeInput: + evt.preventDefault(); + if (this._passcodeBuffer === '') { + this._hideErrorMessage(); + } + + var code = evt.charCode; + if (code !== 0 && (code < 0x30 || code > 0x39)) { + return; + } + + var key = String.fromCharCode(code); + if (evt.charCode === 0) { + if (this._passcodeBuffer.length > 0) { + this._passcodeBuffer = this._passcodeBuffer.substring(0, + this._passcodeBuffer.length - 1); + if (this.passcodePanel.dataset.passcodeStatus === 'success') { + this._resetPasscodeStatus(); + } + } + } else if (this._passcodeBuffer.length < 8) { + this._passcodeBuffer += key; + } + + this._updatePassCodeUI(); + this._enablePasscode(); + break; + case this.createPasscodeButton: + case this.changePasscodeButton: + evt.stopPropagation(); + if (this.passcodePanel.dataset.passcodeStatus !== 'success') { + this._showErrorMessage(); + this.passcodeInput.focus(); + return; + } + passcode = this._passcodeBuffer.substring(0, 4); + settings = navigator.mozSettings; + lock = settings.createLock(); + lock.set({ + 'lockscreen.passcode-lock.code': passcode + }); + lock.set({ + 'lockscreen.passcode-lock.enabled': true + }); + this._backToScreenLock(); + break; + } + }, + + _enablePasscode: function sld_enablePasscode() { + var settings; + var passcode; + var lock; + + if (this._passcodeBuffer.length === this._checkingLength[this._MODE]) { + switch (this._MODE) { + case 'create': + case 'new': + passcode = this._passcodeBuffer.substring(0, 4); + var passcodeToConfirm = this._passcodeBuffer.substring(4, 8); + if (passcode != passcodeToConfirm) { + this._passcodeBuffer = ''; + this._showErrorMessage(); + } else { + this._enableButton(); + } + break; + case 'confirm': + if (this._checkPasscode()) { + settings = navigator.mozSettings; + lock = settings.createLock(); + lock.set({ + 'lockscreen.passcode-lock.enabled': false + }); + this._backToScreenLock(); + } else { + this._passcodeBuffer = ''; + } + break; + case 'confirmLock': + if (this._checkPasscode()) { + settings = navigator.mozSettings; + lock = settings.createLock(); + lock.set({ + 'lockscreen.enabled': false, + 'lockscreen.passcode-lock.enabled': false + }); + this._backToScreenLock(); + } else { + this._passcodeBuffer = ''; + } + break; + case 'edit': + if (this._checkPasscode()) { + this._passcodeBuffer = ''; + this._updatePassCodeUI(); + this._showDialogInMode('new'); + } else { + this._passcodeBuffer = ''; + } + break; + } + } + }, + + _fetchSettings: function sld_fetchSettings() { + SettingsListener.observe('lockscreen.passcode-lock.code', '0000', + function(passcode) { + this._settings.passcode = passcode; + }.bind(this)); + }, + + _showErrorMessage: function sld_showErrorMessage(message) { + this.passcodePanel.dataset.passcodeStatus = 'error'; + }, + + _hideErrorMessage: function sld_hideErrorMessage() { + this.passcodePanel.dataset.passcodeStatus = ''; + }, + + _resetPasscodeStatus: function sld_resetPasscodeStatus() { + this.passcodePanel.dataset.passcodeStatus = ''; + }, + + _enableButton: function sld_enableButton() { + this.passcodePanel.dataset.passcodeStatus = 'success'; + }, + + _updatePassCodeUI: function sld_updatePassCodeUI() { + for (var i = 0; i < 8; i++) { + if (i < this._passcodeBuffer.length) { + this.passcodeDigits[i].dataset.dot = true; + } else { + delete this.passcodeDigits[i].dataset.dot; + } + } + }, + + _checkPasscode: function sld_checkPasscode() { + if (this._settings.passcode != this._passcodeBuffer) { + this._showErrorMessage(); + return false; + } else { + this._hideErrorMessage(); + return true; + } + }, + + _backToScreenLock: function sld_backToScreenLock() { + this._passcodeBuffer = ''; + this.passcodeInput.blur(); + panels.show({ id: 'rpp-screenlock', back: true }); + } + + }; + + return new PassCodePanel(); + +}); diff --git a/apps/privacy-panel/js/rpp/passphrase.js b/apps/privacy-panel/js/rpp/passphrase.js new file mode 100644 index 000000000000..98d49739d6b9 --- /dev/null +++ b/apps/privacy-panel/js/rpp/passphrase.js @@ -0,0 +1,117 @@ +/** + * PassPhrase storage helper. + * + * @module PassPhrase + * @return {Object} + */ +define([ + 'shared/async_storage' +], + +function(asyncStorage) { + 'use strict'; + + const SALT_NUM_BYTES = 8; + + function PassPhrase(macDest, saltDest) { + this.macDest = macDest; + this.saltDest = saltDest; + } + + PassPhrase.prototype = { + buffer: encode('topsecret'), + + _getItem: function(key) { + var promise = new Promise(resolve => { + asyncStorage.getItem(key, resolve); + }); + return promise; + }, + + _setItem: function(key, value) { + var promise = new Promise(resolve => { + asyncStorage.setItem(key, value, () => resolve(value)); + }); + return promise; + }, + + exists: function() { + return this._mac().then(mac => !!mac); + }, + + verify: function(password) { + return this._mac().then(mac => { + if (!mac) { + return false; + } + + return this._retrieveKey(password).then(key => { + return crypto.subtle.verify('HMAC', key, mac, this.buffer); + }); + }); + }, + + change: function(password) { + return this._retrieveKey(password).then(key => { + return crypto.subtle.sign('HMAC', key, this.buffer) + .then(mac => this._setItem(this.macDest, mac)); + }); + }, + + clear: function() { + return this._setItem(this.macDest, null); + }, + + _mac: function() { + return this._getItem(this.macDest); + }, + + _salt: function() { + return this._getItem(this.saltDest).then(salt => { + if (salt) { + return salt; + } + salt = crypto.getRandomValues(new Uint8Array(SALT_NUM_BYTES)); + return this._setItem(this.saltDest, salt); + }); + }, + + _retrievePWKey: function(password) { + var usages = ['deriveKey']; + var buffer = encode(password); + return crypto.subtle.importKey('raw', buffer, 'PBKDF2', false, usages); + }, + + _retrieveKey: function(password) { + var params = Promise.all([ + this._retrievePWKey(password), this._salt() + ]); + + return params.then(values => { + var pwKey = values[0]; + var salt = values[1]; + return this._deriveKey(pwKey, salt); + }); + }, + + _deriveKey: function(pwKey, salt) { + var params = { + name: 'PBKDF2', + hash: 'SHA-1', + salt: salt, + iterations: 5000 + }; + var alg = {name: 'HMAC', hash: 'SHA-256'}; + var usages = ['sign', 'verify']; + return crypto.subtle.deriveKey(params, pwKey, alg, false, usages); + } + + }; + + function encode(str) { + return new TextEncoder('utf-8').encode(str); + } + + return PassPhrase; + +}); diff --git a/apps/privacy-panel/js/rpp/screenlock.js b/apps/privacy-panel/js/rpp/screenlock.js new file mode 100644 index 000000000000..148129a46b14 --- /dev/null +++ b/apps/privacy-panel/js/rpp/screenlock.js @@ -0,0 +1,96 @@ +/** + * Auth panels (login/register/change passphrase). + * + * @module ScreenLockPanel + * @return {Object} + */ +define([ + 'panels', + 'shared/settings_listener', +], + +function(panels, SettingsListener) { + 'use strict'; + + function ScreenLockPanel() {} + + ScreenLockPanel.prototype = { + + _settings: { + passcodeEnabled: false, + lockscreenEnabled: false + }, + + init: function() { + this.panel = document.getElementById('rpp-screenlock'); + + this._getAllElements(); + this.passcodeEnable.addEventListener('click', this); + this.lockscreenEnable.addEventListener('click', this); + this.passcodeEditButton.addEventListener('click', this); + this._fetchSettings(); + }, + + _getAllElements: function sl_getAllElements() { + this.screenlockPanel = this.panel; + this.lockscreenEnable = this.panel.querySelector('.lockscreen-enable'); + this.passcodeEnable = this.panel.querySelector('.passcode-enable'); + this.passcodeEditButton = this.panel.querySelector('.passcode-edit'); + }, + + _fetchSettings: function sl_fetchSettings() { + SettingsListener.observe('lockscreen.enabled', false, + function(enabled) { + this._toggleLockscreen(enabled); + }.bind(this)); + + SettingsListener.observe('lockscreen.passcode-lock.enabled', false, + function(enabled) { + this._togglePasscode(enabled); + }.bind(this)); + }, + + _togglePasscode: function sl_togglePasscode(enabled) { + this._settings.passcodeEnabled = enabled; + this.screenlockPanel.dataset.passcodeEnabled = enabled; + this.passcodeEnable.checked = enabled; + }, + + _toggleLockscreen: function sl_toggleLockscreen(enabled) { + this._settings.lockscreenEnabled = enabled; + this.screenlockPanel.dataset.lockscreenEnabled = enabled; + this.lockscreenEnable.checked = enabled; + }, + + _showDialog: function sl_showDialog(mode) { + panels.show({ id: 'rpp-passcode', options: mode }); + }, + + handleEvent: function sl_handleEvent(evt) { + switch (evt.target) { + case this.passcodeEnable: + evt.preventDefault(); + if (this._settings.passcodeEnabled) { + this._showDialog('confirm'); + } else { + this._showDialog('create'); + } + break; + case this.lockscreenEnable: + if (this._settings.lockscreenEnabled === true && + this._settings.passcodeEnabled === true) { + evt.preventDefault(); + this._showDialog('confirmLock'); + } + break; + case this.passcodeEditButton: + this._showDialog('edit'); + break; + } + } + + }; + + return new ScreenLockPanel(); + +}); diff --git a/apps/privacy-panel/js/sms/commands.js b/apps/privacy-panel/js/sms/commands.js new file mode 100644 index 000000000000..5d0d5a525fee --- /dev/null +++ b/apps/privacy-panel/js/sms/commands.js @@ -0,0 +1,160 @@ +/** + * Command module to handle lock, ring, locate features. + * + * @module Commands + * @return {Object} + */ +define([ + 'shared/settings_listener', + 'shared/settings_helper', + 'shared/settings_url' +], + +function(SettingsListener, SettingsHelper, SettingsURL) { + 'use strict'; + + var Commands = { + TRACK_UPDATE_INTERVAL_MS: 10000, + + _ringer: null, + + _lockscreenEnabled: false, + + _lockscreenPassCodeEnabled: false, + + _geolocationEnabled: false, + + init: function fmdc_init() { + var ringer = this._ringer = new Audio(); + ringer.mozAudioChannelType = 'ringer'; + ringer.loop = true; + + var ringtoneURL = new SettingsURL(); + SettingsListener.observe('dialer.ringtone', '', function(value) { + var ringing = !ringer.paused; + + ringer.pause(); + ringer.src = ringtoneURL.set(value); + if (ringing) { + ringer.play(); + } + }); + + var self = this; + SettingsListener.observe('lockscreen.enabled', false, function(value) { + self._lockscreenEnabled = value; + }); + + SettingsListener.observe('lockscreen.passcode-lock.enabled', false, + function(value) { + self._lockscreenPassCodeEnabled = value; + } + ); + + SettingsListener.observe('geolocation.enabled', false, function(value) { + self._geolocationEnabled = value; + }); + }, + + invokeCommand: function fmdc_get_command(name, args) { + this._commands[name].apply(this, args); + }, + + deviceHasPasscode: function fmdc_device_has_passcode() { + return !!(this._lockscreenEnabled && this._lockscreenPassCodeEnabled); + }, + + _ringTimeoutId: null, + + _commands: { + locate: function fmdc_track(duration, reply) { + var options = { + enableHighAccuracy: true, + timeout: duration * 1000, + maximumAge: 0 + }; + + reply = reply || function() {}; + + function success(position) { + reply(true, position); + } + + function error(err) { + reply(false, err.message); + } + + navigator.geolocation.getCurrentPosition(success, error, options); + }, + + lock: function fmdc_lock(message, passcode, reply) { + var settings = { + 'lockscreen.enabled': true, + 'lockscreen.notifications-preview.enabled': false, + 'lockscreen.passcode-lock.enabled': true, + 'lockscreen.lock-immediately': true + }; + + if (message) { + settings['lockscreen.lock-message'] = message; + } + + if (!this.deviceHasPasscode() && passcode) { + settings['lockscreen.passcode-lock.code'] = passcode; + } + + var request = SettingsListener.getSettingsLock().set(settings); + request.onsuccess = function() { + reply(true); + }; + + request.onerror = function() { + reply(false, 'failed to set settings'); + }; + }, + + ring: function fmdc_ring(duration, reply) { + var ringer = this._ringer; + + var stop = function() { + ringer.pause(); + ringer.currentTime = 0; + clearTimeout(this._ringTimeoutId); + this._ringTimeoutId = null; + }.bind(this); + + var ringing = !ringer.paused || this._ringTimeoutId !== null; + if (ringing || duration === 0) { + if (ringing && duration === 0) { + stop(); + } + + if (reply) { + reply(true); + } + return; + } + + var request = SettingsListener.getSettingsLock().set({ + // hard-coded max volume taken from + // https://wiki.mozilla.org/WebAPI/AudioChannels + 'audio.volume.notification': 15 + }); + + request.onsuccess = function() { + ringer.play(); + reply(true); + }; + + request.onerror = function() { + reply(false, 'failed to set volume'); + }; + + this._ringTimeoutId = setTimeout(stop, duration * 1000); + } + } + }; + + return Commands; + +}); diff --git a/apps/privacy-panel/js/sms/main.js b/apps/privacy-panel/js/sms/main.js new file mode 100644 index 000000000000..9c744c696303 --- /dev/null +++ b/apps/privacy-panel/js/sms/main.js @@ -0,0 +1,227 @@ +/** + * Command module to handle lock, ring, locate features. + * + * @module RPPExecuteCommands + * @return {Object} + */ +define([ + 'sms/commands', + 'rpp/passphrase', + 'shared/settings_listener', + 'shared/settings_helper' +], + +function(Commands, PassPhrase, SettingsListener, SettingsHelper) { + 'use strict'; + + const RING_ENABLED = 'rpp.ring.enabled'; + const LOCK_ENABLED = 'rpp.lock.enabled'; + const LOCATE_ENABLED = 'rpp.locate.enabled'; + const PASSCODE_ENABLED = 'lockscreen.passcode-lock.enabled'; + const LOCKSCREEN_ENABLED = 'lockscreen.enabled'; + const LOCKSCREEN_LOCKED = 'lockscreen.locked'; + + var RPPExecuteCommands = { + + _ringEnabled: false, + _lockEnabled: false, + _locateEnabled: false, + _passcodeEnabled : false, + _lockscreenEnabled : false, + + init: function() { + Commands.init(); + this.passphrase = new PassPhrase('rppmac', 'rppsalt'); + + this.observers(); + this.events(); + }, + + observers: function() { + SettingsListener.observe(LOCKSCREEN_ENABLED, false, value => { + this._lockscreenEnabled = value; + }); + + SettingsListener.observe(PASSCODE_ENABLED, false, value => { + this._passcodeEnabled = value; + }); + + SettingsListener.observe(RING_ENABLED, false, value => { + this._ringEnabled = value; + }); + + SettingsListener.observe(LOCK_ENABLED, false, value => { + this._lockEnabled = value; + }); + + SettingsListener.observe(LOCATE_ENABLED, false, value => { + this._locateEnabled = value; + }); + + SettingsListener.observe(LOCKSCREEN_LOCKED, false, value => { + if (!value) { + Commands.invokeCommand('ring', [0]); + } + }); + }, + + events: function() { + navigator.mozSetMessageHandler('sms-received', + this._onSMSReceived.bind(this)); + }, + + /** + * Search for RPP commands and execute them. + * + * @param {Object} event Object recieved from SMS listener event 'recieved' + */ + _onSMSReceived: function(event) { + var match, cmd, passkey, body = event.body, + rgx = /^rpp\s(lock|ring|locate)\s([a-z0-9]{1,100})$/i, + sender = event.sender; + + // If there is no passcode, do nothing. + if (!this._passcodeEnabled || !this._lockscreenEnabled) { + return; + } + + match = body.match(rgx); + + if (match) { + cmd = match[1]; + passkey = match[2]; + + this.passphrase.verify(passkey).then(function(status) { + if (!status) { + return; + } + + switch(cmd.toLowerCase()) { + case 'lock': + this._lock(sender); + break; + case 'ring': + this._ring(sender); + break; + case 'locate': + this._locate(sender); + break; + default: + break; + } + }.bind(this)); + } + }, + + _sendSMS : function(number, messageL10n) { + var message; + if (typeof(messageL10n) === 'string') { + message = navigator.mozL10n.get(messageL10n); + } else if (messageL10n.id) { + message = navigator.mozL10n.get(messageL10n.id, messageL10n.args); + } else { + return; + } + + if (navigator.mozMobileMessage) { + navigator.mozMobileMessage.send(number, message); + } + }, + + /** + * Remotely rings the device + * + * @param {Number} number Phone number + */ + _ring : function(number) { + if (!this._ringEnabled) { + return; + } + + var ringReply = function(res, err) { + if (!res) { + console.warn('Error while trying to ring a phone, ' + err); + return; + } + + this._sendSMS(number, 'sms-ring'); + + // Lock phone + setTimeout(function() { + this._doLock(number); + }.bind(this), 3000); + }.bind(this); + + Commands.invokeCommand('ring', [600, ringReply]); + }, + + /** + * Remotely locks the screen + * + * @param {Number} number Phone number + */ + _lock : function(number) { + if (!this._lockEnabled) { + return; + } + + var lockReply = function(status, result) { + if (!status) { + console.warn('Error while trying to lock a phone, ' + result); + return; + } + this._sendSMS(number, 'sms-lock'); + }.bind(this); + + // Lock screen + this._doLock(number, lockReply); + }, + + /** + * Remotely locates device and sends back reply SMS. + * + * @param {Number} number Phone number + */ + _locate : function(number) { + if (!this._locateEnabled) { + return; + } + + var locateReply = function(status, result) { + if (!status) { + console.warn('Error while trying to locate a phone: ' + result); + return; + } + + this._sendSMS(number, { + id: 'sms-locate', + args: { + latitude: result.coords.latitude, + longitude: result.coords.longitude + } + }); + + // Lock phone + setTimeout(function() { + this._doLock(number); + }.bind(this), 3000); + }.bind(this); + + Commands.invokeCommand('locate', [10, locateReply]); + }, + + /** + * Perform lockscreen + * + * @param {Number} number Phone number + */ + _doLock : function(number, reply) { + reply = reply || function() {}; + Commands.invokeCommand('lock', [null, null, reply]); + } + + }; + + return RPPExecuteCommands; + +}); diff --git a/apps/privacy-panel/js/vendor/alameda.js b/apps/privacy-panel/js/vendor/alameda.js new file mode 100644 index 000000000000..a8713561f5a2 --- /dev/null +++ b/apps/privacy-panel/js/vendor/alameda.js @@ -0,0 +1,1414 @@ +/** + * alameda 0.2.0 Copyright (c) 2011-2014, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/requirejs/alameda for details + */ +//Going sloppy to avoid 'use strict' string cost, but strict practices should +//be followed. +/*jslint sloppy: true, nomen: true, regexp: true */ +/*global setTimeout, process, document, navigator, importScripts, + setImmediate */ + +var requirejs, require, define; +(function (global, undef) { + var prim, topReq, dataMain, src, subPath, + bootstrapConfig = requirejs || require, + hasOwn = Object.prototype.hasOwnProperty, + contexts = {}, + queue = [], + currDirRegExp = /^\.\//, + urlRegExp = /^\/|\:|\?|\.js$/, + commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg, + cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, + jsSuffixRegExp = /\.js$/; + + if (typeof requirejs === 'function') { + return; + } + + function hasProp(obj, prop) { + return hasOwn.call(obj, prop); + } + + function getOwn(obj, prop) { + return obj && hasProp(obj, prop) && obj[prop]; + } + + /** + * Cycles over properties in an object and calls a function for each + * property value. If the function returns a truthy value, then the + * iteration is stopped. + */ + function eachProp(obj, func) { + var prop; + for (prop in obj) { + if (hasProp(obj, prop)) { + if (func(obj[prop], prop)) { + break; + } + } + } + } + + /** + * Simple function to mix in properties from source into target, + * but only if target does not already have a property of the same name. + */ + function mixin(target, source, force, deepStringMixin) { + if (source) { + eachProp(source, function (value, prop) { + if (force || !hasProp(target, prop)) { + if (deepStringMixin && typeof value === 'object' && value && + !Array.isArray(value) && typeof value !== 'function' && + !(value instanceof RegExp)) { + + if (!target[prop]) { + target[prop] = {}; + } + mixin(target[prop], value, force, deepStringMixin); + } else { + target[prop] = value; + } + } + }); + } + return target; + } + + //Allow getting a global that expressed in + //dot notation, like 'a.b.c'. + function getGlobal(value) { + if (!value) { + return value; + } + var g = global; + value.split('.').forEach(function (part) { + g = g[part]; + }); + return g; + } + + //START prim 0.0.6 + /** + * Changes from baseline prim + * - removed UMD registration + */ + (function () { + 'use strict'; + + var waitingId, nextTick, + waiting = []; + + function callWaiting() { + waitingId = 0; + var w = waiting; + waiting = []; + while (w.length) { + w.shift()(); + } + } + + function asyncTick(fn) { + waiting.push(fn); + if (!waitingId) { + waitingId = setTimeout(callWaiting, 0); + } + } + + function syncTick(fn) { + fn(); + } + + function isFunObj(x) { + var type = typeof x; + return type === 'object' || type === 'function'; + } + + //Use setImmediate.bind() because attaching it (or setTimeout directly + //to prim will result in errors. Noticed first on IE10, + //issue requirejs/alameda#2) + nextTick = typeof setImmediate === 'function' ? setImmediate.bind() : + (typeof process !== 'undefined' && process.nextTick ? + process.nextTick : (typeof setTimeout !== 'undefined' ? + asyncTick : syncTick)); + + function notify(ary, value) { + prim.nextTick(function () { + ary.forEach(function (item) { + item(value); + }); + }); + } + + function callback(p, ok, yes) { + if (p.hasOwnProperty('v')) { + prim.nextTick(function () { + yes(p.v); + }); + } else { + ok.push(yes); + } + } + + function errback(p, fail, no) { + if (p.hasOwnProperty('e')) { + prim.nextTick(function () { + no(p.e); + }); + } else { + fail.push(no); + } + } + + prim = function prim(fn) { + var promise, f, + p = {}, + ok = [], + fail = []; + + function makeFulfill() { + var f, f2, + called = false; + + function fulfill(v, prop, listeners) { + if (called) { + return; + } + called = true; + + if (promise === v) { + called = false; + f.reject(new TypeError('value is same promise')); + return; + } + + try { + var then = v && v.then; + if (isFunObj(v) && typeof then === 'function') { + f2 = makeFulfill(); + then.call(v, f2.resolve, f2.reject); + } else { + p[prop] = v; + notify(listeners, v); + } + } catch (e) { + called = false; + f.reject(e); + } + } + + f = { + resolve: function (v) { + fulfill(v, 'v', ok); + }, + reject: function(e) { + fulfill(e, 'e', fail); + } + }; + return f; + } + + f = makeFulfill(); + + promise = { + then: function (yes, no) { + var next = prim(function (nextResolve, nextReject) { + + function finish(fn, nextFn, v) { + try { + if (fn && typeof fn === 'function') { + v = fn(v); + nextResolve(v); + } else { + nextFn(v); + } + } catch (e) { + nextReject(e); + } + } + + callback(p, ok, finish.bind(undefined, yes, nextResolve)); + errback(p, fail, finish.bind(undefined, no, nextReject)); + + }); + return next; + }, + + catch: function (no) { + return promise.then(null, no); + } + }; + + try { + fn(f.resolve, f.reject); + } catch (e) { + f.reject(e); + } + + return promise; + }; + + prim.resolve = function (value) { + return prim(function (yes) { + yes(value); + }); + }; + + prim.reject = function (err) { + return prim(function (yes, no) { + no(err); + }); + }; + + prim.cast = function (x) { + // A bit of a weak check, want "then" to be a function, + // but also do not want to trigger a getter if accessing + // it. Good enough for now. + if (isFunObj(x) && 'then' in x) { + return x; + } else { + return prim(function (yes, no) { + if (x instanceof Error) { + no(x); + } else { + yes(x); + } + }); + } + }; + + prim.all = function (ary) { + return prim(function (yes, no) { + var count = 0, + length = ary.length, + result = []; + + function resolved(i, v) { + result[i] = v; + count += 1; + if (count === length) { + yes(result); + } + } + + ary.forEach(function (item, i) { + prim.cast(item).then(function (v) { + resolved(i, v); + }, function (err) { + no(err); + }); + }); + }); + }; + + prim.nextTick = nextTick; + }()); + //END prim + + function newContext(contextName) { + var req, main, makeMap, callDep, handlers, checkingLater, load, context, + defined = {}, + waiting = {}, + config = { + //Defaults. Do not set a default for map + //config to speed up normalize(), which + //will run faster if there is no default. + waitSeconds: 7, + baseUrl: './', + paths: {}, + bundles: {}, + pkgs: {}, + shim: {}, + config: {} + }, + mapCache = {}, + requireDeferreds = [], + deferreds = {}, + calledDefine = {}, + calledPlugin = {}, + loadCount = 0, + startTime = (new Date()).getTime(), + errCount = 0, + trackedErrors = {}, + urlFetched = {}, + bundlesMap = {}; + + /** + * Trims the . and .. from an array of path segments. + * It will keep a leading path segment if a .. will become + * the first path segment, to help with module name lookups, + * which act like paths, but can be remapped. But the end result, + * all paths that use this function should look normalized. + * NOTE: this method MODIFIES the input array. + * @param {Array} ary the array of path segments. + */ + function trimDots(ary) { + var i, part, length = ary.length; + for (i = 0; i < length; i++) { + part = ary[i]; + if (part === '.') { + ary.splice(i, 1); + i -= 1; + } else if (part === '..') { + if (i === 1 && (ary[2] === '..' || ary[0] === '..')) { + //End of the line. Keep at least one non-dot + //path segment at the front so it can be mapped + //correctly to disk. Otherwise, there is likely + //no path mapping for a path starting with '..'. + //This can still fail, but catches the most reasonable + //uses of .. + break; + } else if (i > 0) { + ary.splice(i - 1, 2); + i -= 2; + } + } + } + } + + /** + * Given a relative module name, like ./something, normalize it to + * a real name that can be mapped to a path. + * @param {String} name the relative name + * @param {String} baseName a real name that the name arg is relative + * to. + * @param {Boolean} applyMap apply the map config to the value. Should + * only be done if this normalization is for a dependency ID. + * @returns {String} normalized name + */ + function normalize(name, baseName, applyMap) { + var pkgMain, mapValue, nameParts, i, j, nameSegment, lastIndex, + foundMap, foundI, foundStarMap, starI, + baseParts = baseName && baseName.split('/'), + normalizedBaseParts = baseParts, + map = config.map, + starMap = map && map['*']; + + //Adjust any relative paths. + if (name && name.charAt(0) === '.') { + //If have a base name, try to normalize against it, + //otherwise, assume it is a top-level require that will + //be relative to baseUrl in the end. + if (baseName) { + //Convert baseName to array, and lop off the last part, + //so that . matches that 'directory' and not name of the baseName's + //module. For instance, baseName of 'one/two/three', maps to + //'one/two/three.js', but we want the directory, 'one/two' for + //this normalization. + normalizedBaseParts = baseParts.slice(0, baseParts.length - 1); + name = name.split('/'); + lastIndex = name.length - 1; + + // If wanting node ID compatibility, strip .js from end + // of IDs. Have to do this here, and not in nameToUrl + // because node allows either .js or non .js to map + // to same file. + if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { + name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); + } + + name = normalizedBaseParts.concat(name); + trimDots(name); + name = name.join('/'); + } else if (name.indexOf('./') === 0) { + // No baseName, so this is ID is resolved relative + // to baseUrl, pull off the leading dot. + name = name.substring(2); + } + } + + //Apply map config if available. + if (applyMap && map && (baseParts || starMap)) { + nameParts = name.split('/'); + + outerLoop: for (i = nameParts.length; i > 0; i -= 1) { + nameSegment = nameParts.slice(0, i).join('/'); + + if (baseParts) { + //Find the longest baseName segment match in the config. + //So, do joins on the biggest to smallest lengths of baseParts. + for (j = baseParts.length; j > 0; j -= 1) { + mapValue = getOwn(map, baseParts.slice(0, j).join('/')); + + //baseName segment has config, find if it has one for + //this name. + if (mapValue) { + mapValue = getOwn(mapValue, nameSegment); + if (mapValue) { + //Match, update name to the new value. + foundMap = mapValue; + foundI = i; + break outerLoop; + } + } + } + } + + //Check for a star map match, but just hold on to it, + //if there is a shorter segment match later in a matching + //config, then favor over this star map. + if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) { + foundStarMap = getOwn(starMap, nameSegment); + starI = i; + } + } + + if (!foundMap && foundStarMap) { + foundMap = foundStarMap; + foundI = starI; + } + + if (foundMap) { + nameParts.splice(0, foundI, foundMap); + name = nameParts.join('/'); + } + } + + // If the name points to a package's name, use + // the package main instead. + pkgMain = getOwn(config.pkgs, name); + + return pkgMain ? pkgMain : name; + } + + function makeShimExports(value) { + function fn() { + var ret; + if (value.init) { + ret = value.init.apply(global, arguments); + } + return ret || (value.exports && getGlobal(value.exports)); + } + return fn; + } + + function takeQueue(anonId) { + var i, id, args, shim; + for (i = 0; i < queue.length; i += 1) { + //Peek to see if anon + if (typeof queue[i][0] !== 'string') { + if (anonId) { + queue[i].unshift(anonId); + anonId = undef; + } else { + //Not our anon module, stop. + break; + } + } + args = queue.shift(); + id = args[0]; + i -= 1; + + if (!hasProp(defined, id) && !hasProp(waiting, id)) { + if (hasProp(deferreds, id)) { + main.apply(undef, args); + } else { + waiting[id] = args; + } + } + } + + //if get to the end and still have anonId, then could be + //a shimmed dependency. + if (anonId) { + shim = getOwn(config.shim, anonId) || {}; + main(anonId, shim.deps || [], shim.exportsFn); + } + } + + function makeRequire(relName, topLevel) { + var req = function (deps, callback, errback, alt) { + var name, cfg; + + if (topLevel) { + takeQueue(); + } + + if (typeof deps === "string") { + if (handlers[deps]) { + return handlers[deps](relName); + } + //Just return the module wanted. In this scenario, the + //deps arg is the module name, and second arg (if passed) + //is just the relName. + //Normalize module name, if it contains . or .. + name = makeMap(deps, relName, true).id; + if (!hasProp(defined, name)) { + throw new Error('Not loaded: ' + name); + } + return defined[name]; + } else if (deps && !Array.isArray(deps)) { + //deps is a config object, not an array. + cfg = deps; + deps = undef; + + if (Array.isArray(callback)) { + //callback is an array, which means it is a dependency list. + //Adjust args if there are dependencies + deps = callback; + callback = errback; + errback = alt; + } + + if (topLevel) { + //Could be a new context, so call returned require + return req.config(cfg)(deps, callback, errback); + } + } + + //Support require(['a']) + callback = callback || function () {}; + + //Simulate async callback; + prim.nextTick(function () { + //Grab any modules that were defined after a + //require call. + takeQueue(); + main(undef, deps || [], callback, errback, relName); + }); + + return req; + }; + + req.isBrowser = typeof document !== 'undefined' && + typeof navigator !== 'undefined'; + + req.nameToUrl = function (moduleName, ext, skipExt) { + var paths, syms, i, parentModule, url, + parentPath, bundleId, + pkgMain = getOwn(config.pkgs, moduleName); + + if (pkgMain) { + moduleName = pkgMain; + } + + bundleId = getOwn(bundlesMap, moduleName); + + if (bundleId) { + return req.nameToUrl(bundleId, ext, skipExt); + } + + //If a colon is in the URL, it indicates a protocol is used and it is just + //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?) + //or ends with .js, then assume the user meant to use an url and not a module id. + //The slash is important for protocol-less URLs as well as full paths. + if (urlRegExp.test(moduleName)) { + //Just a plain path, not module name lookup, so just return it. + //Add extension if it is included. This is a bit wonky, only non-.js things pass + //an extension, this method probably needs to be reworked. + url = moduleName + (ext || ''); + } else { + //A module that needs to be converted to a path. + paths = config.paths; + + syms = moduleName.split('/'); + //For each module name segment, see if there is a path + //registered for it. Start with most specific name + //and work up from it. + for (i = syms.length; i > 0; i -= 1) { + parentModule = syms.slice(0, i).join('/'); + + parentPath = getOwn(paths, parentModule); + if (parentPath) { + //If an array, it means there are a few choices, + //Choose the one that is desired + if (Array.isArray(parentPath)) { + parentPath = parentPath[0]; + } + syms.splice(0, i, parentPath); + break; + } + } + + //Join the path parts together, then figure out if baseUrl is needed. + url = syms.join('/'); + url += (ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js')); + url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url; + } + + return config.urlArgs ? url + + ((url.indexOf('?') === -1 ? '?' : '&') + + config.urlArgs) : url; + }; + + /** + * Converts a module name + .extension into an URL path. + * *Requires* the use of a module name. It does not support using + * plain URLs like nameToUrl. + */ + req.toUrl = function (moduleNamePlusExt) { + var ext, + index = moduleNamePlusExt.lastIndexOf('.'), + segment = moduleNamePlusExt.split('/')[0], + isRelative = segment === '.' || segment === '..'; + + //Have a file extension alias, and it is not the + //dots from a relative path. + if (index !== -1 && (!isRelative || index > 1)) { + ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length); + moduleNamePlusExt = moduleNamePlusExt.substring(0, index); + } + + return req.nameToUrl(normalize(moduleNamePlusExt, relName), ext, true); + }; + + req.defined = function (id) { + return hasProp(defined, makeMap(id, relName, true).id); + }; + + req.specified = function (id) { + id = makeMap(id, relName, true).id; + return hasProp(defined, id) || hasProp(deferreds, id); + }; + + return req; + } + + function resolve(name, d, value) { + if (name) { + defined[name] = value; + if (requirejs.onResourceLoad) { + requirejs.onResourceLoad(context, d.map, d.deps); + } + } + d.finished = true; + d.resolve(value); + } + + function reject(d, err) { + d.finished = true; + d.rejected = true; + d.reject(err); + } + + function makeNormalize(relName) { + return function (name) { + return normalize(name, relName, true); + }; + } + + function defineModule(d) { + var name = d.map.id, + ret = d.factory.apply(defined[name], d.values); + + if (name) { + // Favor return value over exports. If node/cjs in play, + // then will not have a return value anyway. Favor + // module.exports assignment over exports object. + if (ret === undef) { + if (d.cjsModule) { + ret = d.cjsModule.exports; + } else if (d.usingExports) { + ret = defined[name]; + } + } + } else { + //Remove the require deferred from the list to + //make cycle searching faster. Do not need to track + //it anymore either. + requireDeferreds.splice(requireDeferreds.indexOf(d), 1); + } + resolve(name, d, ret); + } + + //This method is attached to every module deferred, + //so the "this" in here is the module deferred object. + function depFinished(val, i) { + if (!this.rejected && !this.depDefined[i]) { + this.depDefined[i] = true; + this.depCount += 1; + this.values[i] = val; + if (!this.depending && this.depCount === this.depMax) { + defineModule(this); + } + } + } + + function makeDefer(name) { + var d = {}; + d.promise = prim(function (resolve, reject) { + d.resolve = resolve; + d.reject = reject; + }); + d.map = name ? makeMap(name, null, true) : {}; + d.depCount = 0; + d.depMax = 0; + d.values = []; + d.depDefined = []; + d.depFinished = depFinished; + if (d.map.pr) { + //Plugin resource ID, implicitly + //depends on plugin. Track it in deps + //so cycle breaking can work + d.deps = [makeMap(d.map.pr)]; + } + return d; + } + + function getDefer(name) { + var d; + if (name) { + d = hasProp(deferreds, name) && deferreds[name]; + if (!d) { + d = deferreds[name] = makeDefer(name); + } + } else { + d = makeDefer(); + requireDeferreds.push(d); + } + return d; + } + + function makeErrback(d, name) { + return function (err) { + if (!d.rejected) { + if (!err.dynaId) { + err.dynaId = 'id' + (errCount += 1); + err.requireModules = [name]; + } + reject(d, err); + } + }; + } + + function waitForDep(depMap, relName, d, i) { + d.depMax += 1; + + //Do the fail at the end to catch errors + //in the then callback execution. + callDep(depMap, relName).then(function (val) { + d.depFinished(val, i); + }, makeErrback(d, depMap.id)).catch(makeErrback(d, d.map.id)); + } + + function makeLoad(id) { + var fromTextCalled; + function load(value) { + //Protect against older plugins that call load after + //calling load.fromText + if (!fromTextCalled) { + resolve(id, getDefer(id), value); + } + } + + load.error = function (err) { + getDefer(id).reject(err); + }; + + load.fromText = function (text, textAlt) { + /*jslint evil: true */ + var d = getDefer(id), + map = makeMap(makeMap(id).n), + plainId = map.id; + + fromTextCalled = true; + + //Set up the factory just to be a return of the value from + //plainId. + d.factory = function (p, val) { + return val; + }; + + //As of requirejs 2.1.0, support just passing the text, to reinforce + //fromText only being called once per resource. Still + //support old style of passing moduleName but discard + //that moduleName in favor of the internal ref. + if (textAlt) { + text = textAlt; + } + + //Transfer any config to this other module. + if (hasProp(config.config, id)) { + config.config[plainId] = config.config[id]; + } + + try { + req.exec(text); + } catch (e) { + reject(d, new Error('fromText eval for ' + plainId + + ' failed: ' + e)); + } + + //Execute any waiting define created by the plainId + takeQueue(plainId); + + //Mark this as a dependency for the plugin + //resource + d.deps = [map]; + waitForDep(map, null, d, d.deps.length); + }; + + return load; + } + + load = typeof importScripts === 'function' ? + function (map) { + var url = map.url; + if (urlFetched[url]) { + return; + } + urlFetched[url] = true; + + //Ask for the deferred so loading is triggered. + //Do this before loading, since loading is sync. + getDefer(map.id); + importScripts(url); + takeQueue(map.id); + } : + function (map) { + var script, + id = map.id, + url = map.url; + + if (urlFetched[url]) { + return; + } + urlFetched[url] = true; + + script = document.createElement('script'); + script.setAttribute('data-requiremodule', id); + script.type = config.scriptType || 'text/javascript'; + script.charset = 'utf-8'; + script.async = true; + + loadCount += 1; + + script.addEventListener('load', function () { + loadCount -= 1; + takeQueue(id); + }, false); + script.addEventListener('error', function () { + loadCount -= 1; + var err, + pathConfig = getOwn(config.paths, id), + d = getOwn(deferreds, id); + if (pathConfig && Array.isArray(pathConfig) && pathConfig.length > 1) { + script.parentNode.removeChild(script); + //Pop off the first array value, since it failed, and + //retry + pathConfig.shift(); + d.map = makeMap(id); + load(d.map); + } else { + err = new Error('Load failed: ' + id + ': ' + script.src); + err.requireModules = [id]; + getDefer(id).reject(err); + } + }, false); + + script.src = url; + + document.head.appendChild(script); + }; + + function callPlugin(plugin, map, relName) { + plugin.load(map.n, makeRequire(relName), makeLoad(map.id), {}); + } + + callDep = function (map, relName) { + var args, bundleId, + name = map.id, + shim = config.shim[name]; + + if (hasProp(waiting, name)) { + args = waiting[name]; + delete waiting[name]; + main.apply(undef, args); + } else if (!hasProp(deferreds, name)) { + if (map.pr) { + //If a bundles config, then just load that file instead to + //resolve the plugin, as it is built into that bundle. + if ((bundleId = getOwn(bundlesMap, name))) { + map.url = req.nameToUrl(bundleId); + load(map); + } else { + return callDep(makeMap(map.pr)).then(function (plugin) { + //Redo map now that plugin is known to be loaded + var newMap = makeMap(name, relName, true), + newId = newMap.id, + shim = getOwn(config.shim, newId); + + //Make sure to only call load once per resource. Many + //calls could have been queued waiting for plugin to load. + if (!hasProp(calledPlugin, newId)) { + calledPlugin[newId] = true; + if (shim && shim.deps) { + req(shim.deps, function () { + callPlugin(plugin, newMap, relName); + }); + } else { + callPlugin(plugin, newMap, relName); + } + } + return getDefer(newId).promise; + }); + } + } else if (shim && shim.deps) { + req(shim.deps, function () { + load(map); + }); + } else { + load(map); + } + } + + return getDefer(name).promise; + }; + + //Turns a plugin!resource to [plugin, resource] + //with the plugin being undefined if the name + //did not have a plugin prefix. + function splitPrefix(name) { + var prefix, + index = name ? name.indexOf('!') : -1; + if (index > -1) { + prefix = name.substring(0, index); + name = name.substring(index + 1, name.length); + } + return [prefix, name]; + } + + /** + * Makes a name map, normalizing the name, and using a plugin + * for normalization if necessary. Grabs a ref to plugin + * too, as an optimization. + */ + makeMap = function (name, relName, applyMap) { + if (typeof name !== 'string') { + return name; + } + + var plugin, url, parts, prefix, result, + cacheKey = name + ' & ' + (relName || '') + ' & ' + !!applyMap; + + parts = splitPrefix(name); + prefix = parts[0]; + name = parts[1]; + + if (!prefix && hasProp(mapCache, cacheKey)) { + return mapCache[cacheKey]; + } + + if (prefix) { + prefix = normalize(prefix, relName, applyMap); + plugin = hasProp(defined, prefix) && defined[prefix]; + } + + //Normalize according + if (prefix) { + if (plugin && plugin.normalize) { + name = plugin.normalize(name, makeNormalize(relName)); + } else { + name = normalize(name, relName, applyMap); + } + } else { + name = normalize(name, relName, applyMap); + parts = splitPrefix(name); + prefix = parts[0]; + name = parts[1]; + + url = req.nameToUrl(name); + } + + //Using ridiculous property names for space reasons + result = { + id: prefix ? prefix + '!' + name : name, //fullName + n: name, + pr: prefix, + url: url + }; + + if (!prefix) { + mapCache[cacheKey] = result; + } + + return result; + }; + + handlers = { + require: function (name) { + return makeRequire(name); + }, + exports: function (name) { + var e = defined[name]; + if (typeof e !== 'undefined') { + return e; + } else { + return (defined[name] = {}); + } + }, + module: function (name) { + return { + id: name, + uri: '', + exports: handlers.exports(name), + config: function () { + return getOwn(config.config, name) || {}; + } + }; + } + }; + + function breakCycle(d, traced, processed) { + var id = d.map.id; + + traced[id] = true; + if (!d.finished && d.deps) { + d.deps.forEach(function (depMap) { + var depId = depMap.id, + dep = !hasProp(handlers, depId) && getDefer(depId); + + //Only force things that have not completed + //being defined, so still in the registry, + //and only if it has not been matched up + //in the module already. + if (dep && !dep.finished && !processed[depId]) { + if (hasProp(traced, depId)) { + d.deps.forEach(function (depMap, i) { + if (depMap.id === depId) { + d.depFinished(defined[depId], i); + } + }); + } else { + breakCycle(dep, traced, processed); + } + } + }); + } + processed[id] = true; + } + + function check(d) { + var err, + notFinished = [], + waitInterval = config.waitSeconds * 1000, + //It is possible to disable the wait interval by using waitSeconds of 0. + expired = waitInterval && (startTime + waitInterval) < (new Date()).getTime(); + + if (loadCount === 0) { + //If passed in a deferred, it is for a specific require call. + //Could be a sync case that needs resolution right away. + //Otherwise, if no deferred, means a nextTick and all + //waiting require deferreds should be checked. + if (d) { + if (!d.finished) { + breakCycle(d, {}, {}); + } + } else if (requireDeferreds.length) { + requireDeferreds.forEach(function (d) { + breakCycle(d, {}, {}); + }); + } + } + + //If still waiting on loads, and the waiting load is something + //other than a plugin resource, or there are still outstanding + //scripts, then just try back later. + if (expired) { + //If wait time expired, throw error of unloaded modules. + eachProp(deferreds, function (d) { + if (!d.finished) { + notFinished.push(d.map.id); + } + }); + err = new Error('Timeout for modules: ' + notFinished); + err.requireModules = notFinished; + req.onError(err); + } else if (loadCount || requireDeferreds.length) { + //Something is still waiting to load. Wait for it, but only + //if a later check is not already scheduled. + if (!checkingLater) { + checkingLater = true; + prim.nextTick(function () { + checkingLater = false; + check(); + }); + } + } + } + + //Used to break out of the promise try/catch chains. + function delayedError(e) { + prim.nextTick(function () { + if (!e.dynaId || !trackedErrors[e.dynaId]) { + trackedErrors[e.dynaId] = true; + req.onError(e); + } + }); + } + + main = function (name, deps, factory, errback, relName) { + //Only allow main calling once per module. + if (name && hasProp(calledDefine, name)) { + return; + } + calledDefine[name] = true; + + var d = getDefer(name); + + //This module may not have dependencies + if (deps && !Array.isArray(deps)) { + //deps is not an array, so probably means + //an object literal or factory function for + //the value. Adjust args. + factory = deps; + deps = []; + } + + d.promise.catch(errback || delayedError); + + //Use name if no relName + relName = relName || name; + + //Call the factory to define the module, if necessary. + if (typeof factory === 'function') { + + if (!deps.length && factory.length) { + //Remove comments from the callback string, + //look for require calls, and pull them into the dependencies, + //but only if there are function args. + factory + .toString() + .replace(commentRegExp, '') + .replace(cjsRequireRegExp, function (match, dep) { + deps.push(dep); + }); + + //May be a CommonJS thing even without require calls, but still + //could use exports, and module. Avoid doing exports and module + //work though if it just needs require. + //REQUIRES the function to expect the CommonJS variables in the + //order listed below. + deps = (factory.length === 1 ? + ['require'] : + ['require', 'exports', 'module']).concat(deps); + } + + //Save info for use later. + d.factory = factory; + d.deps = deps; + + d.depending = true; + deps.forEach(function (depName, i) { + var depMap; + deps[i] = depMap = makeMap(depName, relName, true); + depName = depMap.id; + + //Fast path CommonJS standard dependencies. + if (depName === "require") { + d.values[i] = handlers.require(name); + } else if (depName === "exports") { + //CommonJS module spec 1.1 + d.values[i] = handlers.exports(name); + d.usingExports = true; + } else if (depName === "module") { + //CommonJS module spec 1.1 + d.values[i] = d.cjsModule = handlers.module(name); + } else if (depName === undefined) { + d.values[i] = undefined; + } else { + waitForDep(depMap, relName, d, i); + } + }); + d.depending = false; + + //Some modules just depend on the require, exports, modules, so + //trigger their definition here if so. + if (d.depCount === d.depMax) { + defineModule(d); + } + } else if (name) { + //May just be an object definition for the module. Only + //worry about defining if have a module name. + resolve(name, d, factory); + } + + startTime = (new Date()).getTime(); + + if (!name) { + check(d); + } + }; + + req = makeRequire(null, true); + + /* + * Just drops the config on the floor, but returns req in case + * the config return value is used. + */ + req.config = function (cfg) { + if (cfg.context && cfg.context !== contextName) { + return newContext(cfg.context).config(cfg); + } + + //Since config changed, mapCache may not be valid any more. + mapCache = {}; + + //Make sure the baseUrl ends in a slash. + if (cfg.baseUrl) { + if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') { + cfg.baseUrl += '/'; + } + } + + //Save off the paths and packages since they require special processing, + //they are additive. + var primId, + shim = config.shim, + objs = { + paths: true, + bundles: true, + config: true, + map: true + }; + + eachProp(cfg, function (value, prop) { + if (objs[prop]) { + if (!config[prop]) { + config[prop] = {}; + } + mixin(config[prop], value, true, true); + } else { + config[prop] = value; + } + }); + + //Reverse map the bundles + if (cfg.bundles) { + eachProp(cfg.bundles, function (value, prop) { + value.forEach(function (v) { + if (v !== prop) { + bundlesMap[v] = prop; + } + }); + }); + } + + //Merge shim + if (cfg.shim) { + eachProp(cfg.shim, function (value, id) { + //Normalize the structure + if (Array.isArray(value)) { + value = { + deps: value + }; + } + if ((value.exports || value.init) && !value.exportsFn) { + value.exportsFn = makeShimExports(value); + } + shim[id] = value; + }); + config.shim = shim; + } + + //Adjust packages if necessary. + if (cfg.packages) { + cfg.packages.forEach(function (pkgObj) { + var location, name; + + pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj; + + name = pkgObj.name; + location = pkgObj.location; + if (location) { + config.paths[name] = pkgObj.location; + } + + //Save pointer to main module ID for pkg name. + //Remove leading dot in main, so main paths are normalized, + //and remove any trailing .js, since different package + //envs have different conventions: some use a module name, + //some use a file name. + config.pkgs[name] = pkgObj.name + '/' + (pkgObj.main || 'main') + .replace(currDirRegExp, '') + .replace(jsSuffixRegExp, ''); + }); + } + + //If want prim injected, inject it now. + primId = config.definePrim; + if (primId) { + waiting[primId] = [primId, [], function () { return prim; }]; + } + + //If a deps array or a config callback is specified, then call + //require with those args. This is useful when require is defined as a + //config object before require.js is loaded. + if (cfg.deps || cfg.callback) { + req(cfg.deps, cfg.callback); + } + + return req; + }; + + req.onError = function (err) { + throw err; + }; + + context = { + id: contextName, + defined: defined, + waiting: waiting, + config: config, + deferreds: deferreds + }; + + contexts[contextName] = context; + + return req; + } + + requirejs = topReq = newContext('_'); + + if (typeof require !== 'function') { + require = topReq; + } + + /** + * Executes the text. Normally just uses eval, but can be modified + * to use a better, environment-specific call. Only used for transpiling + * loader plugins, not for plain JS modules. + * @param {String} text the text to execute/evaluate. + */ + topReq.exec = function (text) { + /*jslint evil: true */ + return eval(text); + }; + + topReq.contexts = contexts; + + define = function () { + queue.push([].slice.call(arguments, 0)); + }; + + define.amd = { + jQuery: true + }; + + if (bootstrapConfig) { + topReq.config(bootstrapConfig); + } + + //data-main support. + if (topReq.isBrowser && !contexts._.config.skipDataMain) { + dataMain = document.querySelectorAll('script[data-main]')[0]; + dataMain = dataMain && dataMain.getAttribute('data-main'); + if (dataMain) { + //Strip off any trailing .js since dataMain is now + //like a module name. + dataMain = dataMain.replace(jsSuffixRegExp, ''); + + if (!bootstrapConfig || !bootstrapConfig.baseUrl) { + //Pull off the directory of data-main for use as the + //baseUrl. + src = dataMain.split('/'); + dataMain = src.pop(); + subPath = src.length ? src.join('/') + '/' : './'; + + topReq.config({baseUrl: subPath}); + } + + topReq([dataMain]); + } + } +}(this)); diff --git a/apps/privacy-panel/locales/countries.en-US.properties b/apps/privacy-panel/locales/countries.en-US.properties new file mode 100644 index 000000000000..07929c738ca9 --- /dev/null +++ b/apps/privacy-panel/locales/countries.en-US.properties @@ -0,0 +1,455 @@ +#Regions/Cities + +africa = Africa +abidjan = Abidjan +accra = Accra +addis-ababa = Addis Ababa +algiers = Algiers +asmara = Asmara +bamako = Bamako +bangui = Bangui +banjul = Banjul +bissau = Bissau +blantyre = Blantyre +blantyre = Blantyre +brazzaville = Brazzaville +bujumbura = Bujumbura +cairo = Cairo +casablanca = Casablanca +ceuta = Ceuta +conakry = Conakry +dakar = Dakar +dar-es-salaam = Dar es Salaam +djibouti = Djibouti +douala = Douala +el-aaiun = El Aaiun +freetown = Freetown +gaborone = Gaborone +harare = Harare +johannesburg = Johannesburg +juba = Juba +kampala = Kampala +khartoum = Khartoum +kigali = Kigali +kinshasa = Kinshasa +lagos = Lagos +libreville = Libreville +lome = Lome +luanda = Luanda +lubumbashi = Lubumbashi +lusaka = Lusaka +malabo = Malabo +maputo = Maputo +maseru = Maseru +mbabane = Mbabane +mogadishu = Mogadishu +monrovia = Monrovia +nairobi = Nairobi +ndjamena = Ndjamena +nouakchott = Nouakchott +ouagadougou = Ouagadougou +porto-novo = Porto-Novo +sao-tome = Sao Tome +timbuktu = Timbuktu +tripoli = Tripoli +tunis = Tunis +windhoek = Windhoek + +america = America +adak = Adak +anchorage = Anchorage +anguilla = Anguilla +antigua = Antigua +araguaina = Araguaina +aruba = Aruba +asuncion = Asuncion +atikokan = Atikokan +atka = Atka +bahia = Bahia +bahia-banderas = Bahia Banderas +barbados = Barbados +belem = Belem +belize = Belize +blanc-sablon = Blanc-Sablon +boa-vista = Boa Vista +bogota = Bogotá +boise = Boise +brasilia = Brasília +buelah = Buelah +buenos-aires = Buenos Aires +caamaraca = Caamaraca +cambridge-bay = Cambridge Bay +cambo-grande = Cambo Grande +cancun = Cancun +caracas = Caracas +cayenne = Cayenne +chicago = Chicago +chihuahua = Chihuahua +comodoro-rivadavia = Comodoro Rivadavia +coral-harbour = Coral Harbour +cordoba = Cordoba +costa-rica = Costa Rica +creston = Creston +creston = Creston +cuiaba = Cuiaba +curacao = Curacao +danmarkshavn = Danmarkshavn +dawson = Dawson +dawson-creek = Dawson Creek +denver = Denver +detroit = Detroit +dominica = Dominica +edmonton = Edmonton +eirunepe = Eirunepe +el-salvador = El Salvador +ensenada = Ensenada +fort-wayne = Fort Wayne +fortaleza = Fortaleza +glace-bay = Glace Bay +godthab = Godthab +goose-bay = Goose Bay +grand-turk = Grand Turk +grenada = Grenada +guadeloupe = Guadeloupe +guatemala = Guatemala +guayaquil = Guayaquil +guyana = Guyana +halifax = Halifax +havana = Havana +hermosillo = Hermosillo +indianapolis = Indianapolis +inuvik = Inuvik +iqaluit = Iqaluit +jamaica = Jamaica +jujuy = Jujuy +juneau = Juneau +knox = Knox +kralendijk = Kralendijk +la-paz = La Paz +la-rioja = La Rioja +lima = Lima +los-angeles = Los Angeles +louisville = Louisville +lower-princes = Lower Princes +maceio = Maceio +managua = Managua +manaus = Manaus +marigot = Marigot +martinique = Martinique +matamoros = Matamoros +mazatlan = Mazatlan +mendoza = Mendoza +menominee = Menominee +merida = Merida +metlakatla = Metlakatla +mexico-city = Mexico City +marengo = Marengo +miquelon = Miquelon +moncton = Moncton +monterrey = Monterrey +montevideo = Montevideo +monticello = Monticello +montreal = Montreal +montserrat = Montserrat +nassau = Nassau +new-salem = New Salem +new-york = New York +nipigon = Nipigon +nome = Nome +noronha = Noronha +ojinaga = Ojinaga +panama = Panama +pangnirtung = Pangnirtung +paramaribo = Paramaribo +petersburg = Petersburg +phoenix = Phoenix +port-of-spain = Port of Spain +port-au-prince = Port-au-Prince +porto-acre = Porto Acre +porto-velho = Porto Velho +perto-rico = Perto Rico +quito = Quito +rainy-river = Rainy River +rankin-inlet = Rankin Inlet +recife = Recife +regina = Regina +resolute = Resolute +rio-branco = Rio Branco +rio-gallegos = Rio Gallegos +rosario = Rosario +salta = Salta +san-juan = San Juan +san-luis = San Luis +santa-isabel = Santa Isabel +santarem = Santarem +santiago = Santiago +santo-domingo = Santo Domingo +scoresbysund = Scoresbysund +shiprock = Shiprock +shiprock = Shiprock +sitka = Sitka +st-barthelemy = St Barthelemy +st-johns = St Johns +st-kitts = St Kitts +st-lucia = St Lucia +st-thomas = St Thomas +st-vincent = St Vincent +swift-current = Swift Current +sao-paulo = São Paulo +tell-city = Tell City +tegucigalpa = Tegucigalpa +thule = Thule +thunder-bay = Thunder Bay +tucuman = Tucuman +tijuana = Tijuana +toronto = Toronto +tortola = Tortola +ushuaia = Ushuaia +vancouver = Vancouver +vevay = Vevay +vincennes = Vincennes +virgin = Virgin +whitehorse = Whitehorse +winamac = Winamac +winnipeg = Winnipeg +yakutat = Yakutat +yellowknife = Yellowknife + +antarctica = Antarctica +casey = Casey +davis = Davis +dumont-d-urville = Dumont d'Urville +macquarie = Macquarie +mawson = Mawson +mcmurdo = McMurdo +palmer = Palmer +rothera = Rothera +south-pole = South Pole +syowa = Syowa +vostok = Vostok + +asia = Asia +aden = Aden +almaty = Almaty +amman = Amman +anadyr = Anadyr +aqtau = Aqtau +aqtobe = Aqtobe +ashgabat = Ashgabat +ashkhabad = Ashkhabad +baghdad = Baghdad +bahrain = Bahrain +baku = Baku +bangkok = Bangkok +beirut = Beirut +bishkek = Bishkek +brunei = Brunei +choibalsan = Choibalsan +chongqing = Chongqing +colombo = Colombo +damascus = Damascus +dhaka = Dhaka +dili = Dili +dubai = Dubai +dushanbe = Dushanbe +gaza = Gaza +harbin = Harbin +herbon = Herbon +ho-chi-minh = Ho Chi Minh +hong-kong = Hong Kong +hovd = Hovd +irkutsk = Irkutsk +istanbul = Istanbul +jakarta = Jakarta +jayapura = Jayapura +jerusalem = Jerusalem +kabul = Kabul +kamchatka = Kamchatka +karachi = Karachi +kashgar = Kashgar +kathmandu = Kathmandu +kolkata = Kolkata +krasnoyarsk = Krasnoyarsk +kuala-lumpur = Kuala Lumpur +kuching = Kuching +kuwait = Kuwait +macau = Macau +magadan = Magadan +makassar = Makassar +manila = Manila +muscat = Muscat +nicosia = Nicosia +novokuznetsk = Novokuznetsk +novosibirsk = Novosibirsk +omsk = Omsk +oral = Oral +phnom-penh = Phnom Penh +pontianak = Pontianak +pyongyang = Pyongyang +qatar = Qatar +qyzylorda = Qyzylorda +rangoon = Rangoon +riyadh = Riyadh +saigon = Saigon +sakhalin = Sakhalin +samarkand = Samarkand +seoul = Seoul +shanghai = Shanghai +singapore = Singapore +taipei = Taipei +tashkent = Tashkent +tbilisi = Tbilisi +tehran = Tehran +tel-aviv = Tel Aviv +thimphu = Thimphu +tokyo = Tokyo +ulan-bator = Ulan Bator +urumqi = Urumqi +vientiane = Vientiane +vladivostok = Vladivostok +yakutsk = Yakutsk +yekaterinburg = Yekaterinburg +yerevan = Yerevan + +atlantic-ocean = Atlantic Ocean +azores = Azores +bermuda = Bermuda +canary = Canary +cape-verde = Cape Verde +faroe-islands = Faroe Islands +jan-mayen = Jan Mayen +madeira = Madeira +reykjavik = Reykjavik +south-georgia = South Georgia +st-helena = St Helena +stanley = Stanley + +australia = Australia +adelaide = Adelaide +brisbane = Brisbane +broken-hill = Broken Hill +currie = Currie +darwin = Darwin +eucla = Eucla +hobart = Hobart +lindeman = Lindeman +lord-howe = Lord Howe +melbourne = Melbourne +perth = Perth +sydney = Sydney + +europe = Europe +amsterdam = Amsterdam +andorra = Andorra +athens = Athens +belfast = Belfast +belgrade = Belgrade +berlin = Berlin +bratislava = Bratislava +brussels = Brussels +bucharest = Bucharest +budapest = Budapest +chisinau = Chisinau +copenhagen = Copenhagen +dublin = Dublin +gilbraltar = Gilbraltar +guerney = Guerney +helsinki = Helsinki +isle-of-man = Isle of Man +istanbul = Istanbul +jersey = Jersey +kaliningrad = Kaliningrad +kiev = Kiev +lisbon = Lisbon +ljubljana = Ljubljana +london = London +luxembourg = Luxembourg +madrid = Madrid +malta = Malta +mariehamn = Mariehamn +minsk = Minsk +monaco = Monaco +moscow = Moscow +nicosia = Nicosia +oslo = Oslo +paris = Paris +podgorica = Podgorica +prague = Prague +riga = Riga +rome = Rome +samara = Samara +san-marino = San Marino +sarajevo = Sarajevo +simferopol = Simferopol +skopje = Skopje +sofia = Sofia +stockholm = Stockholm +tallinn = Tallinn +tirane = Tirane +tiraspol = Tiraspol +uzhgorod = Uzhgorod +vaduz = Vaduz +vatican = Vatican +vienna = Vienna +vilnius = Vilnius +volgograd = Volgograd +warsaw = Warsaw +zagreb = Zagreb +zaporozhye = Zaporozhye +zurich = Zurich + +indian-ocean = Indian Ocean +antonanarivo = Antonanarivo +chagos = Chagos +christmas = Christmas +cocos = Cocos +comoro = Comoro +kerguelen = Kerguelen +mahe = Mahe +maldives = Maldives +mauritius = Mauritius +mayotte = Mayotte +reunion = Reunion + +pacific-ocean = Pacific Ocean +apia = Apia +auckland = Auckland +chatham = Chatham +chuuk-lagoon = Chuuk Lagoon +easter = Easter +efate = Efate +enderbury = Enderbury +fakaofo = Fakaofo +fiji = Fiji +funafuti = Funafuti +galapagos = Galapagos +gambier = Gambier +guadalcanal = Guadalcanal +guam = Guam +honolulu = Honolulu +johnston = Johnston +kritimati = Kritimati +kosrae = Kosrae +kwajalein = Kwajalein +majuro = Majuro +marquesas = Marquesas +midway = Midway +nauru = Nauru +niue = Niue +norfolk = Norfolk +noumea = Noumea +pago-pago = Pago Pago +palau = Palau +pitcairn = Pitcairn +pohnpei = Pohnpei +ponape = Ponape +port-moresby = Port Moresby +rarotonga = Rarotonga +saipan = Saipan +samoa = Samoa +tahiti = Tahiti +tarawa = Tarawa +tongatapu = Tongatapu +wake = Wake +wallis = Wallis +yap = Yap diff --git a/apps/privacy-panel/locales/privacypanel.en-US.properties b/apps/privacy-panel/locales/privacypanel.en-US.properties new file mode 100644 index 000000000000..2421d705202f --- /dev/null +++ b/apps/privacy-panel/locales/privacypanel.en-US.properties @@ -0,0 +1,213 @@ +# Main Page +privacy-panel = Privacy Panel +location-accuracy = Location Accuracy +remote-privacy-protection = Remote Privacy Protection +guided-tour = Guided Tour + + +# ALA-3 (ALA: Adjust Location Accuracy) +use-geolocation = Use geolocation +use-location-adjustment = Use location adjustment +location-adjustment-information = Remember: Location Adjustment influences the location Firefox OS provides to apps. It will not affect your IP-address or locale settings, so some services might still be able to locate you. +location-adjustment-description = Adjust the global accuracy level of your current location for all apps, define and set a permanent fixed location and add exceptions for single apps. +adjustment = Adjustment +add-exceptions = Add exceptions + + +# ALA-4 (ALA: Exception List) +app-list-description = This list shows all the apps, which are allowed to use your location. + + +# ALA-5 (ALA: No Custom Location Alert) +attention = Attention! +ala-custom-location-alert = You haven't set a custom location yet. Please set this before working on other settings! + + +# ALA-6 (ALA: Custom Location) +custom-location = Custom Location +set-custom-location = Set custom location +custom-location-description = Set a custom location which is used as a fixed position. + + +# ALA-7 (ALA: Precise Location) +precise = Precise + + +# ALA-8 (ALA: No Location) +no-location = No Location + + +# ALA-9 (ALA: Exception App) +exceptions-application-description = Adjust the custom location accuracy level of the following app: + + +# ALA-10 (ALA: Define Custom Location) +define-custom-location = Define custom location +choose-a-region-city = Choose a region/city +region = Region +city = City +enter-gps-coordinates = Enter GPS Coordinates +enter-gps-description = Please enter the GPS coordinates for longitude from -180.0 to 180 and latitude from -90.0 to 90.0 degree with a dot to separate full degrees lower values. +latitude = Latitude +longitude = Longitude + + +# ALA-13 (ALA: System Default) +global-settings = Global Settings + + + +# RPP-1 (RPP: Register) +rpp-panel = Remote Privacy Protection +rpp-new-password-description = Please set a passphrase to secure your remote features (maximum 100 characters). +new-passphrase.placeholder = New passphrase +confirm-new-passphrase.placeholder = Confirm new passphrase + + +# RPP-2 (RPP: Login) +rpp-login-description = Please enter your passphrase. +rpp-change-password = Forgot/Change your passphrase? +passphrase.placeholder = Passphrase + + +# RPP-3 (RPP: Reset/Change PassPhrase) +rpp-change-password-description = To reset your passphrase please enter Passcode lock/SIM PIN and new passphrase. +sim1 = SIM 1 +sim2 = SIM 2 +enter-sim1.placeholder = Enter SIM 1 Pin Code +enter-sim2.placeholder = Enter SIM 2 Pin Code +enter-passcode.placeholder = Enter PassCode + + +# RPP-1, RPP-2, RPP-3 (RPP: Validation) +passphrase-wrong = Passphrase is wrong! +passphrase-empty = Passphrase is empty! +passphrase-too-long = Passphrase is too long! +passphrase-invalid = You cannot use special characters! +passphrase-different = Confirmation must match passphrase! +pin-empty = Passcode lock is empty! +pin-invalid = Wrong Passcode lock! +pin-different = Wrong Passcode lock! +sim-invalid = Wrong SIM PIN! + + +# RPP-4 (RPP: Features) +remote-locate = Remote Locate +remote-locate-desc = You can locate this device via SMS as following: “RPP locate YOURPASSPHRASE” +remote-ring = Remote Ring +remote-ring-desc = You can ring this device via SMS as following: “RPP ring YOURPASSPHRASE” +remote-lock = Remote Lock +remote-lock-desc = You can lock this device via SMS as following: “RPP lock YOURPASSPHRASE” + + +# RPP-5 (RPP: No LockScreen Alert) +rpp-lockscreen-alert = Please activate lockscreen AND passcode in the system settings to use the Remote Privacy Protection. + + +# RPP-7 (RPP: Screen Lock) +lock-screen = Screen Lock +screen-lock-header = Screen Lock +passcode-lock = Passcode Lock +require-passcode = Require passcode +immediately = Immediately +after-one-minute = After 1 minute +after-five-minutes = After 5 minutes +after-fifteen-minutes = After 15 minutes +after-thirty-minutes = After 30 minutes +after-one-hour = After 1 hour +change-passcode = Change passcode +change = Change +create = Create +passcode-heading = Passcode +current-passcode = Current passcode +new-passcode = New passcode +enter-passcode = Enter passcode +create-a-passcode = Create a Passcode +passcode = Passcode +confirm-passcode = Confirm Passcode +incorrect-passcode = Passcode is incorrect +passcode-doesnt-match = Passcode doesn’t match. Try again. + + + +# GT-1 (Guided Tour intro- screen) +gt-main-header = Welcome to the Privacy Panel! +gt-main-desc = This app will help you enhance your privacy protection and enables you to lock or find your phone if it's lost. Just have a closer look. It takes only 2 minutes! + + +# GT-2 (GT: ALA intro screen) +gt-ala-explain-header = What is Location Adjustment good for? +gt-ala-explain-desc = Many apps access your geolocation, like the addressbook or the camera. If you don't want to disclose your exact position, you can adjust the accuracy of your current location that is used by apps, set a custom location or hide your geolocation. + + +# GT-3 (GT: ALA general settings screen ) +gt-ala-blur-header = How to set Location Adjustment? +gt-ala-blur-desc = Not every app needs your exact location to work properly. Instead of sharing your exact coordinates, you can choose to effectively blur your location to e.g. 50 miles around you. This way, your weather app will still work, but you are not disclosing your exact location. Remember: Location Adjustment will not completely hide you! + + +# GT-4 (GT: ALA custom location) +gt-ala-custom-header = What is Custom Location? +gt-ala-custom-desc = With the custom location setting you are able to hide your real location and set it to another place worldwide. You choose where you are! + + +# GT-5 (GT: ALA per-app settings) +gt-ala-exceptions-header = What is location per app? +gt-ala-exceptions-desc = Here you will find an overview of all apps, that can access your geolocation. You can adjust the settings for each app individually. They overwrite the global location accuracy for that app. + + +# GT-6 (GT: RPP intro screen) +gt-rpp-explain-header = Remote Privacy Protection? What is it? +gt-rpp-explain-desc = If your phone is stolen or lost, you can use the Remote Privacy Protection. With another phone you can locate it, let it ring or even lock it remotely. You first have to set your personal passphrase and enable the features you will want to use (locate, ring and lock). + + +# GT-7 (GT: RPP passphrase) +gt-rpp-passphrase-header = How to set your personal passphrase? +gt-rpp-passphrase-desc = Tap "Remote Privacy Protection" and set your passphrase to secure your remote features. If you have forgotten your actual passphrase you can reset it using your SIM PIN or lockscreen passcode. + + +# GT-8 (GT: RPP locate phone) +gt-rpp-locate-header = How to locate my phone? +gt-rpp-locate-desc1 = If your phone is lost, just take the phone of a friend and send a SMS with +gt-rpp-locate-command = “RPP locate YOURPASSPHRASE“ +gt-rpp-locate-desc2 = to your phone number and you‘ll get the exact GPS coordinates via SMS in return. Make sure to set up the passphrase! + + +# GT-9 (GT: RPP ring phone) +gt-rpp-ring-header = How to ring your phone? +gt-rpp-ring-desc1 = It‘s easy! Just send an SMS with +gt-rpp-ring-command = “RPP ring YOURPASSPHRASE” +gt-rpp-ring-desc2 = to your phone number and it will ring until you find it and unlock it. + + +# GT-10 (GT: RPP lock phone) +gt-rpp-lock-header = How to lock my phone? +gt-rpp-lock-desc1 = If you have lost your phone and you want to lock it remotely just send an SMS with +gt-rpp-lock-command = “RPP lock YOURPASSPHRASE” +gt-rpp-lock-desc2 = to your phone number. The phone will lock itself and only you will be able to unlock it with your regular screen passcode. + + + +# Buttons +get-started = Get Started! +finish-tour = Finish Tour +ok = OK +back = Back +next = Next + + +# SMS messages +sms-ring = Your device should ring now and was locked. You can unlock it with your passcode. +sms-lock = Your device was locked. You can unlock it with your passcode. +sms-locate = Your device coordinates are @{{latitude}},{{longitude}} and your device was locked. You can unlock it with your passcode. + + +# About page +about-privacy-panel = About Privacy Panel +version = Version +build-id = Build-ID +about-header = About the Privacy Panel +about-description = Together with Deutsche Telecom the Mozilla Foundation developed the Privacy Panel to enable the user to take back control of the personal data ... + + + + diff --git a/apps/privacy-panel/manifest.webapp b/apps/privacy-panel/manifest.webapp new file mode 100644 index 000000000000..65d6194d6615 --- /dev/null +++ b/apps/privacy-panel/manifest.webapp @@ -0,0 +1,45 @@ +{ + "name": "Privacy Panel", + "description": "Web app Privacy Panel", + "type": "certified", + "launch_path": "/index.html", + "developer": { + "name": "The Gaia Team", + "url": "https://github.com/mozilla-b2g/gaia" + }, + "permissions": { + "geolocation-noprompt":{}, + "audio-channel-ringer": {}, + "mobileconnection":{}, + "settings":{ "access": "readwrite" }, + "webapps-manage":{}, + "sms":{}, + "telephony":{} + }, + "messages": [ + { "sms-received": "/index.html" } + ], + "locales": { + "ar": { + "name": "Privacy Panel", + "description": "Web app Privacy Panel" + }, + "en-US": { + "name": "Privacy Panel", + "description": "Web app Privacy Panel" + }, + "fr": { + "name": "Privacy Panel", + "description": "Web app Privacy Panel" + }, + "zh-TW": { + "name": "Privacy Panel", + "description": "Web app Privacy Panel" + } + }, + "default_locale": "en-US", + "icons": { + "60": "/style/icons/privacy-panel.png" + }, + "orientation": "portrait-primary" +} diff --git a/apps/privacy-panel/resources/about.json b/apps/privacy-panel/resources/about.json new file mode 100644 index 000000000000..2aa26b75c36b --- /dev/null +++ b/apps/privacy-panel/resources/about.json @@ -0,0 +1,4 @@ +{ + "version": "1.0", + "build": "11/25/2014" +} diff --git a/apps/privacy-panel/resources/countries.json b/apps/privacy-panel/resources/countries.json new file mode 100644 index 000000000000..4b22cb5048eb --- /dev/null +++ b/apps/privacy-panel/resources/countries.json @@ -0,0 +1,1749 @@ +{ + "africa": { + "abidjan": { + "lat": 5.316667, + "lon": -4.033333 + }, + "accra": { + "lat": 5.55, + "lon": -0.2 + }, + "addis-ababa": { + "lat": 8.980603, + "lon": 38.757760 + }, + "algiers": { + "lat": 36.752887, + "lon": 3.042048 + }, + "asmara": { + "lat": 15.333333, + "lon": 38.933333 + }, + "bamako": { + "lat": 12.65, + "lon": -8 + }, + "bangui": { + "lat": 4.366667, + "lon": 18.583333 + }, + "banjul": { + "lat": 13.453056, + "lon": -16.5775 + }, + "bissau": { + "lat": 11.881655, + "lon": -15.617794 + }, + "blantyre": { + "lat": -15.786111, + "lon": 35.005833 + }, + "brazzaville": { + "lat": -4.267778, + "lon": 15.291944 + }, + "bujumbura": { + "lat": -3.383333, + "lon": 29.366667 + }, + "cairo": { + "lat": 30.044419, + "lon": 31.235711 + }, + "casablanca": { + "lat": 33.533333, + "lon": -7.583333 + }, + "ceuta": { + "lat": 35.889387, + "lon": -5.321345 + }, + "conakry": { + "lat": 9.509167, + "lon": -13.712222 + }, + "dakar": { + "lat": 14.764504, + "lon": -17.366028 + }, + "dar-es-salaam": { + "lat": -6.8, + "lon": 39.283333 + }, + "djibouti": { + "lat": 11.825138, + "lon": 42.590275 + }, + "douala": { + "lat": 4.05, + "lon": 9.7 + }, + "el-aaiun": { + "lat": 27.147702, + "lon": -13.222516 + }, + "freetown": { + "lat": 8.484444, + "lon": -13.234444 + }, + "gaborone": { + "lat": -24.658056, + "lon": 25.912222 + }, + "harare": { + "lat": -17.863889, + "lon": 31.029722 + }, + "johannesburg": { + "lat": -26.204102, + "lon": 28.047305 + }, + "juba": { + "lat": 4.85, + "lon": 31.6 + }, + "kampala": { + "lat": 0.313611, + "lon": 32.581111 + }, + "khartoum": { + "lat": 15.566667, + "lon": 32.516667 + }, + "kigali": { + "lat": -1.943889, + "lon": 30.059444 + }, + "kinshasa": { + "lat": -4.331666, + "lon": 15.313889 + }, + "lagos": { + "lat": 6.524379, + "lon": 3.379205 + }, + "libreville": { + "lat": 0.3901, + "lon": 9.4544 + }, + "lome": { + "lat": 6.131944, + "lon": 1.222778 + }, + "luanda": { + "lat": -8.838333, + "lon": 13.234444 + }, + "lubumbashi": { + "lat": -11.664722, + "lon": 27.479444 + }, + "lusaka": { + "lat": -15.416667, + "lon": 28.283333 + }, + "malabo": { + "lat": 3.75, + "lon": 8.783332 + }, + "maputo": { + "lat": -25.966667, + "lon": 32.583333 + }, + "maseru": { + "lat": -29.31, + "lon": 27.48 + }, + "mbabane": { + "lat": -26.316667, + "lon": 31.133333 + }, + "mogadishu": { + "lat": 2.033333, + "lon": 45.35 + }, + "monrovia": { + "lat": 6.313333, + "lon": -10.801389 + }, + "nairobi": { + "lat": -1.292065, + "lon": 36.821946 + }, + "ndjamena": { + "lat": 12.113056, + "lon": 15.049167 + }, + "nouakchott": { + "lat": 18.1, + "lon": -15.95 + }, + "ouagadougou": { + "lat": 12.35, + "lon": -1.516667 + }, + "porto-novo": { + "lat": 6.497222, + "lon": 2.605 + }, + "sao-tome": { + "lat": 0.330192, + "lon": 6.733343 + }, + "timbuktu": { + "lat": 16.766588, + "lon": -3.002561 + }, + "tripoli": { + "lat": 32.813311, + "lon": 13.104844 + }, + "tunis": { + "lat": 36.806494, + "lon": 10.181531 + }, + "windhoek": { + "lat": -22.57, + "lon": 17.083611 + } + }, + "america": { + "adak": { + "lat": 51.88, + "lon": -176.658055 + }, + "anchorage": { + "lat": 61.218055, + "lon": -149.900277 + }, + "anguilla": { + "lat": 18.220554, + "lon": -63.068614 + }, + "antigua": { + "lat": 17.074655, + "lon": -61.817520 + }, + "araguaina": { + "lat": -7.192773, + "lon": -48.204826 + }, + "aruba": { + "lat": 12.52111, + "lon": -69.968338 + }, + "asuncion": { + "lat": -25.282197, + "lon": -57.635099 + }, + "atikokan": { + "lat": 48.757169, + "lon": -91.625521 + }, + "atka": { + "lat": 52.162146, + "lon": -174.344639 + }, + "bahia": { + "lat": -12.579738, + "lon": -41.700727 + }, + "bahia-banderas": { + "lat": 24.18039, + "lon": -110.295856 + }, + "barbados": { + "lat": 13.193887, + "lon": -59.543198 + }, + "belem": { + "lat": -1.455754, + "lon": -48.490179 + }, + "belize": { + "lat": 17.189877, + "lon": -88.49765 + }, + "blanc-sablon": { + "lat": 51.426445, + "lon": -57.131314 + }, + "boa-vista": { + "lat": 2.823509, + "lon": -60.675833 + }, + "bogota": { + "lat": 4.598056, + "lon": -74.075833 + }, + "boise": { + "lat": 43.618710, + "lon": -116.214606 + }, + "brasilia": { + "lat": -14.235004, + "lon": -51.92528 + }, + "buelah": { + "lat": 38.075833, + "lon": -104.986111 + }, + "buenos-aires": { + "lat": -34.603723, + "lon": -58.381593 + }, + "caamaraca": { + "lat": -6.454967, + "lon": -78.838264 + }, + "cambo-grande": { + "lat": -20.469710, + "lon": -54.620121 + }, + "cambridge-bay": { + "lat": 69.117222, + "lon": -105.053056 + }, + "cancun": { + "lat": 21.161908, + "lon": -86.851527 + }, + "caracas": { + "lat": 10.469640, + "lon": -66.803718 + }, + "cayenne": { + "lat": 4.9227, + "lon": -52.326899 + }, + "chicago": { + "lat": 41.878113, + "lon": -87.629798 + }, + "chihuahua": { + "lat": 28.632995, + "lon": -106.069100 + }, + "comodoro-rivadavia": { + "lat": -45.867919, + "lon": -67.5 + }, + "coral-harbour": { + "lat": 64.138834, + "lon": -83.169896 + }, + "cordoba": { + "lat": -31.398929, + "lon": -64.182129 + }, + "costa-rica": { + "lat": 9.748916, + "lon": -83.753428 + }, + "creston": { + "lat": 49.095540, + "lon": -116.513507 + }, + "cuiaba": { + "lat": -15.601410, + "lon": -56.097891 + }, + "curacao": { + "lat": 12.16957, + "lon": -68.99002 + }, + "danmarkshavn": { + "lat": 76.766667, + "lon": -18.666667 + }, + "dawson": { + "lat": 31.773500, + "lon": -84.446582 + }, + "dawson-creek": { + "lat": 55.759627, + "lon": -120.237662 + }, + "denver": { + "lat": 39.737567, + "lon": -104.984717 + }, + "detroit": { + "lat": 42.331427, + "lon": -83.045753 + }, + "dominica": { + "lat": 15.414999, + "lon": -61.370976 + }, + "edmonton": { + "lat": 53.544389, + "lon": -113.490926 + }, + "eirunepe": { + "lat": -6.657140, + "lon": -69.866714 + }, + "el-salvador": { + "lat": 13.794185, + "lon": -88.89653 + }, + "ensenada": { + "lat": 31.857778, + "lon": -116.605833 + }, + "fort-wayne": { + "lat": 41.079273, + "lon": -85.139351 + }, + "fortaleza": { + "lat": -3.731861, + "lon": -38.526670 + }, + "glace-bay": { + "lat": 46.196919, + "lon": -59.957004 + }, + "godthab": { + "lat": 64.175, + "lon": -51.738889 + }, + "goose-bay": { + "lat": 53.301682, + "lon": -60.326084 + }, + "grand-turk": { + "lat": 21.459, + "lon": -71.139 + }, + "grenada": { + "lat": 12.1165, + "lon": -61.678999 + }, + "guadeloupe": { + "lat": 16.265, + "lon": -61.550999 + }, + "guatemala": { + "lat": 15.783471, + "lon": -90.230758 + }, + "guayaquil": { + "lat": -2.170997, + "lon": -79.922359 + }, + "guyana": { + "lat": 4.860416, + "lon": -58.93018 + }, + "halifax": { + "lat": 44.648862, + "lon": -63.575319 + }, + "havana": { + "lat": 23.054069, + "lon": -82.345188 + }, + "hermosillo": { + "lat": 29.072967, + "lon": -110.955919 + }, + "indianapolis": { + "lat": 39.768403, + "lon": -86.158068 + }, + "inuvik": { + "lat": 68.360743, + "lon": -133.723017 + }, + "iqaluit": { + "lat": 63.746693, + "lon": -68.516966 + }, + "jamaica": { + "lat": 18.109581, + "lon": -77.297508 + }, + "jujuy": { + "lat": -24.185786, + "lon": -65.299476 + }, + "juneau": { + "lat": 58.301944, + "lon": -134.419722 + }, + "knox": { + "lat": 41.295875, + "lon": -86.625013 + }, + "kralendijk": { + "lat": 12.1507, + "lon": -68.276699 + }, + "la-paz": { + "lat": -16.5, + "lon": -68.149999 + }, + "la-rioja": { + "lat": -29.412800, + "lon": -66.855980 + }, + "lima": { + "lat": -12.046374, + "lon": -77.042793 + }, + "los-angeles": { + "lat": 34.052234, + "lon": -118.243684 + }, + "louisville": { + "lat": 38.252664, + "lon": -85.758455 + }, + "lower-princes": { + "lat": 18.052778, + "lon": -63.0425 + }, + "maceio": { + "lat": -9.649848, + "lon": -35.708949 + }, + "managua": { + "lat": 12.136389, + "lon": -86.251389 + }, + "manaus": { + "lat": -3.119027, + "lon": -60.021731 + }, + "marengo": { + "lat": 42.248633, + "lon": -88.608426 + }, + "marigot": { + "lat": 18.0731, + "lon": -63.082200 + }, + "martinique": { + "lat": 14.641528, + "lon": -61.024174 + }, + "matamoros": { + "lat": 25.869029, + "lon": -97.502737 + }, + "mazatlan": { + "lat": 23.249414, + "lon": -106.411142 + }, + "mendoza": { + "lat": -32.890183, + "lon": -68.844049 + }, + "menominee": { + "lat": 45.107762, + "lon": -87.614273 + }, + "merida": { + "lat": 20.966044, + "lon": -89.627433 + }, + "metlakatla": { + "lat": 55.129166, + "lon": -131.572222 + }, + "mexico-city": { + "lat": 19.432607, + "lon": -99.133208 + }, + "miquelon": { + "lat": 47.040178, + "lon": -56.334878 + }, + "moncton": { + "lat": 46.087816, + "lon": -64.778231 + }, + "monterrey": { + "lat": 25.686614, + "lon": -100.316112 + }, + "montevideo": { + "lat": -34.901112, + "lon": -56.164531 + }, + "monticello": { + "lat": 38.008604, + "lon": -78.453199 + }, + "montreal": { + "lat": 45.508669, + "lon": -73.553992 + }, + "montserrat": { + "lat": 16.742498, + "lon": -62.187366 + }, + "nassau": { + "lat": 25.06, + "lon": -77.345 + }, + "new-salem": { + "lat": 42.504255, + "lon": -72.332028 + }, + "new-york": { + "lat": 40.712783, + "lon": -74.005941 + }, + "nipigon": { + "lat": 49.015735, + "lon": -88.268316 + }, + "nome": { + "lat": 64.501111, + "lon": -165.406388 + }, + "noronha": { + "lat": -15.447367, + "lon": -50.362688 + }, + "ojinaga": { + "lat": 29.564443, + "lon": -104.416389 + }, + "panama": { + "lat": 8.537981, + "lon": -80.782127 + }, + "pangnirtung": { + "lat": 66.146557, + "lon": -65.701218 + }, + "paramaribo": { + "lat": 5.852035, + "lon": -55.203827 + }, + "perto-rico": { + "lat": 18.220833, + "lon": -66.590149 + }, + "petersburg": { + "lat": 37.227927, + "lon": -77.401927 + }, + "phoenix": { + "lat": 33.448377, + "lon": -112.074037 + }, + "port-au-prince": { + "lat": 18.533333, + "lon": -72.333333 + }, + "port-of-spain": { + "lat": 10.666667, + "lon": -61.516667 + }, + "porto-acre": { + "lat": -9.593638, + "lon": -67.541107 + }, + "porto-velho": { + "lat": -9.414588, + "lon": -64.147823 + }, + "quito": { + "lat": -0.180653, + "lon": -78.467838 + }, + "rainy-river": { + "lat": 48.716667, + "lon": -94.566667 + }, + "rankin-inlet": { + "lat": 62.808375, + "lon": -92.085285 + }, + "recife": { + "lat": -8.047545, + "lon": -34.876962 + }, + "regina": { + "lat": 50.454722, + "lon": -104.606667 + }, + "resolute": { + "lat": 74.697299, + "lon": -94.829729 + }, + "rio-branco": { + "lat": -9.975377, + "lon": -67.824897 + }, + "rio-gallegos": { + "lat": -51.623048, + "lon": -69.216829 + }, + "rosario": { + "lat": -32.950740, + "lon": -60.666500 + }, + "salta": { + "lat": -24.782932, + "lon": -65.412155 + }, + "san-juan": { + "lat": -31.527273, + "lon": -68.521408 + }, + "san-luis": { + "lat": -33.302220, + "lon": -66.336797 + }, + "santa-isabel": { + "lat": -23.317686, + "lon": -46.224110 + }, + "santarem": { + "lat": -2.450629, + "lon": -54.700923 + }, + "santiago": { + "lat": -33.469119, + "lon": -70.641997 + }, + "santo-domingo": { + "lat": 18.466667, + "lon": -69.95 + }, + "sao-paulo": { + "lat": -23.550519, + "lon": -46.633309 + }, + "scoresbysund": { + "lat": 70.485278, + "lon": -21.966667 + }, + "shiprock": { + "lat": 36.785554, + "lon": -108.687032 + }, + "sitka": { + "lat": 57.053055, + "lon": -135.33 + }, + "st-barthelemy": { + "lat": 17.9, + "lon": -62.833333 + }, + "st-johns": { + "lat": 47.560541, + "lon": -52.712831 + }, + "st-kitts": { + "lat": 17.343379, + "lon": -62.755904 + }, + "st-lucia": { + "lat": 13.909444, + "lon": -60.978893 + }, + "st-thomas": { + "lat": 18.335361, + "lon": -64.953400 + }, + "st-vincent": { + "lat": 13.264877, + "lon": -61.210244 + }, + "swift-current": { + "lat": 50.285069, + "lon": -107.797172 + }, + "tegucigalpa": { + "lat": 14.0833, + "lon": -87.2167 + }, + "tell-city": { + "lat": 37.951444, + "lon": -86.767766 + }, + "thule": { + "lat": 77.466667, + "lon": -69.230555 + }, + "thunder-bay": { + "lat": 48.380895, + "lon": -89.247682 + }, + "tijuana": { + "lat": 32.514946, + "lon": -117.038247 + }, + "toronto": { + "lat": 43.653226, + "lon": -79.383184 + }, + "tortola": { + "lat": 18.433470, + "lon": -64.633278 + }, + "tucuman": { + "lat": -26.808284, + "lon": -65.217590 + }, + "ushuaia": { + "lat": -54.801912, + "lon": -68.302951 + }, + "vancouver": { + "lat": 49.261226, + "lon": -123.113926 + }, + "vevay": { + "lat": 38.747840, + "lon": -85.067172 + }, + "vincennes": { + "lat": 38.677269, + "lon": -87.528632 + }, + "virgin": { + "lat": 18.335765, + "lon": -64.896335 + }, + "whitehorse": { + "lat": 60.721187, + "lon": -135.056844 + }, + "winamac": { + "lat": 41.051429, + "lon": -86.603064 + }, + "winnipeg": { + "lat": 49.899754, + "lon": -97.137493 + }, + "yakutat": { + "lat": 59.546944, + "lon": -139.727222 + }, + "yellowknife": {} + }, + "antarctica": { + "casey": { + "lat": -66.281667, + "lon": 110.524444 + }, + "davis": { + "lat": -68.576389, + "lon": 77.968889 + }, + "dumont-d-urville": { + "lat": -66.663026, + "lon": 140.001826 + }, + "macquarie": { + "lat": -54.620811, + "lon": 158.855614 + }, + "mawson": { + "lat": -67.602149, + "lon": 62.872256 + }, + "mcmurdo": { + "lat": -77.841877, + "lon": 166.686344 + }, + "palmer": { + "lat": -64.749579, + "lon": -64.052102 + }, + "rothera": { + "lat": -67.566666, + "lon": -68.133333 + }, + "south-pole": { + "lat": -90, + "lon": 0 + }, + "syowa": { + "lat": -69, + "lon": 39.583333 + }, + "vostok": { + "lat": -78.464491, + "lon": 106.833972 + } + }, + "asia": { + "aden": { + "lat": 12.8, + "lon": 45.033333 + }, + "almaty": { + "lat": 43.2775, + "lon": 76.895833 + }, + "amman": { + "lat": 31.956578, + "lon": 35.945695 + }, + "anadyr": { + "lat": 64.733333, + "lon": 177.516667 + }, + "aqtau": { + "lat": 43.685941, + "lon": 51.287568 + }, + "aqtobe": { + "lat": 50.283333, + "lon": 57.166667 + }, + "ashgabat": { + "lat": 37.933333, + "lon": 58.366667 + }, + "ashkhabad": { + "lat": 37.933333, + "lon": 58.366667 + }, + "baghdad": { + "lat": 33.325, + "lon": 44.422 + }, + "bahrain": { + "lat": 26.0667, + "lon": 50.5577 + }, + "baku": { + "lat": 40.393636, + "lon": 49.863052 + }, + "bangkok": { + "lat": 13.727895, + "lon": 100.524123 + }, + "beirut": { + "lat": 33.888628, + "lon": 35.495479 + }, + "bishkek": { + "lat": 42.874722, + "lon": 74.612222 + }, + "brunei": { + "lat": 4.535277, + "lon": 114.727669 + }, + "choibalsan": { + "lat": 48.078333, + "lon": 114.535 + }, + "chongqing": { + "lat": 29.56301, + "lon": 106.551557 + }, + "colombo": { + "lat": 6.927078, + "lon": 79.861243 + }, + "damascus": { + "lat": 33.513, + "lon": 36.292 + }, + "dhaka": { + "lat": 23.810332, + "lon": 90.412518 + }, + "dili": { + "lat": -8.549999, + "lon": 125.566667 + }, + "dubai": { + "lat": 25.047664, + "lon": 55.181740 + }, + "dushanbe": { + "lat": 38.536667, + "lon": 68.779999 + }, + "gaza": { + "lat": 31.522561, + "lon": 34.453593 + }, + "harbin": { + "lat": 45.803774, + "lon": 126.534967 + }, + "herbon": { + "lat": 47.631389, + "lon": -2.844706 + }, + "ho-chi-minh": { + "lat": 10.823098, + "lon": 106.629663 + }, + "hong-kong": { + "lat": 22.396428, + "lon": 114.109497 + }, + "hovd": { + "lat": 48.004167, + "lon": 91.65 + }, + "irkutsk": { + "lat": 52.286974, + "lon": 104.305018 + }, + "istanbul": { + "lat": 41.00527, + "lon": 28.97696 + }, + "jakarta": { + "lat": -6.208763, + "lon": 106.845599 + }, + "jayapura": { + "lat": -2.533, + "lon": 140.717 + }, + "jerusalem": { + "lat": 31.768319, + "lon": 35.21371 + }, + "kabul": { + "lat": 34.533333, + "lon": 69.166667 + }, + "kamchatka": { + "lat": 61.434398, + "lon": 166.788413 + }, + "karachi": { + "lat": 24.861462, + "lon": 67.009938 + }, + "kashgar": { + "lat": 39.4704, + "lon": 75.989755 + }, + "kathmandu": { + "lat": 27.7, + "lon": 85.333333 + }, + "kolkata": { + "lat": 22.572646, + "lon": 88.363895 + }, + "krasnoyarsk": { + "lat": 56.015283, + "lon": 92.893247 + }, + "kuala-lumpur": { + "lat": 3.139003, + "lon": 101.686855 + }, + "kuching": { + "lat": 1.56, + "lon": 110.345 + }, + "kuwait": { + "lat": 29.31166, + "lon": 47.481766 + }, + "macau": { + "lat": 22.198745, + "lon": 113.543873 + }, + "magadan": { + "lat": 59.566667, + "lon": 150.8 + }, + "makassar": { + "lat": -5.130855, + "lon": 119.416528 + }, + "manila": { + "lat": 14.599512, + "lon": 120.984219 + }, + "muscat": { + "lat": 23.61, + "lon": 58.54 + }, + "nicosia": { + "lat": 35.166667, + "lon": 33.366667 + }, + "novokuznetsk": { + "lat": 53.759593, + "lon": 87.121570 + }, + "novosibirsk": { + "lat": 55.008352, + "lon": 82.935732 + }, + "omsk": { + "lat": 54.983332, + "lon": 73.366666 + }, + "oral": { + "lat": 51.233332, + "lon": 51.366667 + }, + "phnom-penh": { + "lat": 11.544872, + "lon": 104.892166 + }, + "pontianak": { + "lat": 0, + "lon": 109.333333 + }, + "pyongyang": { + "lat": 39.039219, + "lon": 125.762524 + }, + "qatar": { + "lat": 25.354826, + "lon": 51.183884 + }, + "qyzylorda": { + "lat": 44.848831, + "lon": 65.482268 + }, + "rangoon": { + "lat": 16.780833, + "lon": 96.149722 + }, + "riyadh": { + "lat": 24.633333, + "lon": 46.716667 + }, + "saigon": { + "lat": 10.823098, + "lon": 106.629663 + }, + "sakhalin": { + "lat": 51, + "lon": 143 + }, + "samarkand": { + "lat": 39.627012, + "lon": 66.974973 + }, + "seoul": { + "lat": 37.566535, + "lon": 126.977969 + }, + "shanghai": { + "lat": 31.230416, + "lon": 121.473701 + }, + "singapore": { + "lat": 1.352083, + "lon": 103.819836 + }, + "taipei": { + "lat": 25.046565, + "lon": 121.548866 + }, + "tashkent": { + "lat": 41.266667, + "lon": 69.216667 + }, + "tbilisi": { + "lat": 41.716667, + "lon": 44.783333 + }, + "tehran": { + "lat": 35.696111, + "lon": 51.423056 + }, + "tel-aviv": { + "lat": 32.085299, + "lon": 34.781767 + }, + "thimphu": { + "lat": 27.472792, + "lon": 89.639286 + }, + "tokyo": { + "lat": 35.689487, + "lon": 139.691706 + }, + "ulan-bator": { + "lat": 47.919999, + "lon": 106.92 + }, + "urumqi": { + "lat": 43.825592, + "lon": 87.616847 + }, + "vientiane": { + "lat": 17.966667, + "lon": 102.6 + }, + "vladivostok": { + "lat": 43.133333, + "lon": 131.9 + }, + "yakutsk": { + "lat": 62.033332, + "lon": 129.733333 + }, + "yekaterinburg": { + "lat": 56.833333, + "lon": 60.583333 + }, + "yerevan": { + "lat": 40.183333, + "lon": 44.516667 + } + }, + "atlantic-ocean": { + "azores": { + "lat": 37.741248, + "lon": -25.675594 + }, + "bermuda": { + "lat": 32.3078, + "lon": -64.7505 + }, + "canary": { + "lat": 28.291563, + "lon": -16.62913 + }, + "cape-verde": { + "lat": 15.120142, + "lon": -23.605172 + }, + "faroe-islands": { + "lat": 61.892635, + "lon": -6.911805 + }, + "jan-mayen": { + "lat": 71.021353, + "lon": -8.522312 + }, + "madeira": { + "lat": 32.760707, + "lon": -16.959472 + }, + "reykjavik": { + "lat": 64.133333, + "lon": -21.933333 + }, + "south-georgia": { + "lat": -54.413833, + "lon": -36.582716 + }, + "st-helena": { + "lat": -15.965010, + "lon": -5.708924 + }, + "stanley": { + "lat": -51.683333, + "lon": -59.166667 + } + }, + "australia": { + "adelaide": { + "lat": -34.928621, + "lon": 138.599959 + }, + "brisbane": { + "lat": -27.471010, + "lon": 153.023448 + }, + "broken-hill": { + "lat": -31.955858, + "lon": 141.465136 + }, + "currie": { + "lat": -39.928241, + "lon": 143.85231 + }, + "darwin": { + "lat": -12.462827, + "lon": 130.841777 + }, + "eucla": { + "lat": -31.677126, + "lon": 128.889304 + }, + "hobart": { + "lat": -42.881903, + "lon": 147.323814 + }, + "lindeman": { + "lat": -20.444784, + "lon": 149.04105 + }, + "lord-howe": { + "lat": -31.531482, + "lon": 159.070199 + }, + "melbourne": { + "lat": -37.814107, + "lon": 144.96328 + }, + "perth": { + "lat": -31.953004, + "lon": 115.857469 + }, + "sydney": { + "lat": -33.867486, + "lon": 151.206990 + } + }, + "europe": { + "amsterdam": { + "lat": 52.370215, + "lon": 4.895167 + }, + "andorra": { + "lat": 42.506285, + "lon": 1.521801 + }, + "athens": { + "lat": 37.983917, + "lon": 23.729359 + }, + "belfast": { + "lat": 54.597285, + "lon": -5.93012 + }, + "belgrade": { + "lat": 44.816667, + "lon": 20.466667 + }, + "berlin": { + "lat": 52.520006, + "lon": 13.404954 + }, + "bratislava": { + "lat": 48.145892, + "lon": 17.107137 + }, + "brussels": { + "lat": 50.850339, + "lon": 4.35171 + }, + "bucharest": { + "lat": 44.4325, + "lon": 26.103889 + }, + "budapest": { + "lat": 47.497912, + "lon": 19.040235 + }, + "chisinau": { + "lat": 47, + "lon": 28.916667 + }, + "copenhagen": { + "lat": 55.676096, + "lon": 12.568337 + }, + "dublin": { + "lat": 53.349805, + "lon": -6.260309 + }, + "gilbraltar": { + "lat": 36.140751, + "lon": -5.353585 + }, + "guerney": { + "lat": 49.465691, + "lon": -2.585278 + }, + "helsinki": { + "lat": 60.173324, + "lon": 24.941024 + }, + "isle-of-man": { + "lat": 54.236107, + "lon": -4.548056 + }, + "istanbul": { + "lat": 41.00527, + "lon": 28.97696 + }, + "jersey": { + "lat": 49.214439, + "lon": -2.13125 + }, + "kaliningrad": { + "lat": 54.716667, + "lon": 20.516667 + }, + "kiev": { + "lat": 50.4501, + "lon": 30.5234 + }, + "lisbon": { + "lat": 38.722252, + "lon": -9.139336 + }, + "ljubljana": { + "lat": 46.056946, + "lon": 14.505751 + }, + "london": { + "lat": 51.50735, + "lon": -0.127758 + }, + "luxembourg": { + "lat": 49.815273, + "lon": 6.129582 + }, + "madrid": { + "lat": 40.416775, + "lon": -3.70379 + }, + "malta": { + "lat": 35.937496, + "lon": 14.375416 + }, + "mariehamn": { + "lat": 60.097094, + "lon": 19.934833 + }, + "minsk": { + "lat": 53.9, + "lon": 27.566667 + }, + "monaco": { + "lat": 43.738417, + "lon": 7.424615 + }, + "moscow": { + "lat": 55.755826, + "lon": 37.6173 + }, + "nicosia": { + "lat": 35.166667, + "lon": 33.366667 + }, + "oslo": { + "lat": 59.913868, + "lon": 10.752245 + }, + "paris": { + "lat": 48.856614, + "lon": 2.352221 + }, + "podgorica": { + "lat": 42.441286, + "lon": 19.262892 + }, + "prague": { + "lat": 50.075538, + "lon": 14.4378 + }, + "riga": { + "lat": 56.949648, + "lon": 24.105186 + }, + "rome": { + "lat": 41.872388, + "lon": 12.48018 + }, + "samara": { + "lat": 53.202778, + "lon": 50.140833 + }, + "san-marino": { + "lat": 43.94236, + "lon": 12.457777 + }, + "sarajevo": { + "lat": 43.856258, + "lon": 18.413076 + }, + "simferopol": { + "lat": 44.952117, + "lon": 34.102417 + }, + "skopje": { + "lat": 41.997346, + "lon": 21.427995 + }, + "sofia": { + "lat": 42.697708, + "lon": 23.321867 + }, + "stockholm": { + "lat": 59.329323, + "lon": 18.06858 + }, + "tallinn": { + "lat": 59.43696, + "lon": 24.753574 + }, + "tirane": { + "lat": 41.327545, + "lon": 19.818698 + }, + "tiraspol": { + "lat": 46.85, + "lon": 29.633333 + }, + "uzhgorod": { + "lat": 48.6208, + "lon": 22.287883 + }, + "vaduz": { + "lat": 47.141369, + "lon": 9.5207 + }, + "vatican": { + "lat": 41.902916, + "lon": 12.453389 + }, + "vienna": { + "lat": 48.208174, + "lon": 16.373818 + }, + "vilnius": { + "lat": 54.687155, + "lon": 25.279651 + }, + "volgograd": { + "lat": 48.7, + "lon": 44.516667 + }, + "warsaw": { + "lat": 52.229675, + "lon": 21.012228 + }, + "zagreb": { + "lat": 45.81501, + "lon": 15.981919 + }, + "zaporozhye": { + "lat": 47.8388, + "lon": 35.139567 + }, + "zurich": { + "lat": 47.368649, + "lon": 8.539182 + } + }, + "indian-ocean": { + "antonanarivo": { + "lat": -18.933333, + "lon": 47.516667 + }, + "chagos": { + "lat": -6.000000, + "lon": 72 + }, + "christmas": { + "lat": -39.687008, + "lon": 143.832193 + }, + "cocos": { + "lat": -12.170874, + "lon": 96.841739 + }, + "comoro": { + "lat": -8.546661, + "lon": 125.524759 + }, + "kerguelen": { + "lat": -49.25, + "lon": 69.5 + }, + "mahe": { + "lat": -4.682669, + "lon": 55.480396 + }, + "maldives": { + "lat": 1.977247, + "lon": 73.536103 + }, + "mauritius": { + "lat": -20.348404, + "lon": 57.552152 + }, + "mayotte": { + "lat": -12.8275, + "lon": 45.166244 + }, + "reunion": { + "lat": -21.115141, + "lon": 55.536384 + } + }, + "pacific-ocean": { + "apia": { + "lat": -13.833333, + "lon": -171.75 + }, + "auckland": { + "lat": -36.848459, + "lon": 174.763331 + }, + "chatham": { + "lat": -43.9, + "lon": -176.483333 + }, + "chuuk-lagoon": { + "lat": 7.416666, + "lon": 151.783333 + }, + "easter": { + "lat": -27.121192, + "lon": -109.366423 + }, + "efate": { + "lat": -17.657747, + "lon": 168.429718 + }, + "enderbury": { + "lat": -3.133333, + "lon": -171.083333 + }, + "fakaofo": { + "lat": -9.380255, + "lon": -171.218835 + }, + "fiji": { + "lat": -17.713371, + "lon": 178.065032 + }, + "funafuti": { + "lat": -8.516667, + "lon": 179.216667 + }, + "galapagos": { + "lat": -0.829278, + "lon": -90.982066 + }, + "gambier": { + "lat": -23.134916, + "lon": -134.962683 + }, + "guadalcanal": { + "lat": -9.577328, + "lon": 160.145580 + }, + "guam": { + "lat": 13.444304, + "lon": 144.793731 + }, + "honolulu": { + "lat": 21.306944, + "lon": -157.858333 + }, + "johnston": { + "lat": 16.732272, + "lon": -169.530837 + }, + "kosrae": { + "lat": 5.309561, + "lon": 162.981487 + }, + "kritimati": { + "lat": 1.872134, + "lon": -157.427811 + }, + "kwajalein": { + "lat": 9.189823, + "lon": 167.424297 + }, + "majuro": { + "lat": 7.116421, + "lon": 171.185773 + }, + "marquesas": { + "lat": -9.454443, + "lon": -139.388889 + }, + "midway": { + "lat": 28.210076, + "lon": -177.37611 + }, + "nauru": { + "lat": -0.522778, + "lon": 166.931503 + }, + "niue": { + "lat": -19.054445, + "lon": -169.867233 + }, + "norfolk": { + "lat": -29.023064, + "lon": 167.939597 + }, + "noumea": { + "lat": -22.2758, + "lon": 166.458 + }, + "pago-pago": { + "lat": -14.279444, + "lon": -170.700556 + }, + "palau": { + "lat": 7.514979, + "lon": 134.58252 + }, + "pitcairn": { + "lat": -24.376765, + "lon": -128.324339 + }, + "pohnpei": { + "lat": 6.854125, + "lon": 158.262382 + }, + "ponape": { + "lat": 6.854125, + "lon": 158.262382 + }, + "port-moresby": { + "lat": -9.443800, + "lon": 147.180267 + }, + "rarotonga": { + "lat": -21.229237, + "lon": -159.776349 + }, + "saipan": { + "lat": 15.177801, + "lon": 145.750967 + }, + "samoa": { + "lat": -13.759029, + "lon": -172.104629 + }, + "tahiti": { + "lat": -17.650919, + "lon": -149.426042 + }, + "tarawa": { + "lat": 1.329052, + "lon": 172.979052 + }, + "tongatapu": { + "lat": -21.146596, + "lon": -175.251548 + }, + "wake": { + "lat": 19.282319, + "lon": 166.647046 + }, + "wallis": { + "lat": -13.266667, + "lon": -176.2 + }, + "yap": { + "lat": 9.539137, + "lon": 138.125891 + } + } +} diff --git a/apps/privacy-panel/style/icons/privacy-panel.png b/apps/privacy-panel/style/icons/privacy-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..fdfa529386771de7b8f9f9f080f598ec2c949f86 GIT binary patch literal 6560 zcmbVxXH-*Lw{|FkNKr&kK@<^DT0)Q-K!niyp@cwa0RqI3&=PtT1(Bwd&_Suvd+1F; zgh*BCO{(;w2-5tb=bZPuKi+YF+&y+%YtQGIWzDtM*mH;L>#8xI<2(ld0GKt@l?^CY zx!=cGI?5SxJmyBZP`MkZDFI6RuB}iSv<`|oiU2@G4C68K45fV)uL8##I@{wtk!~n} zqMfr1ibn&BbU+!Pkapg#-6#qjjXl~3j)&`LLu{R~K;&;3pcmGKf~IIv@Nz-gVo-P< z8IkMgt&AP7!hIe&F9?MIi^3y$ zys%C0(eivRul~8k(302#U&&q z!NNRZqF^zQC}o!v0gFQ?%p^&(n|#Ae(PTbWt6+E8`=ercE<7iW<=UJ6Y%o96ifeh z0qdfp^B-cI`#*uAgbd_`bOC{Zq982xw_ksx-SGyf|8K^Bm3BAsc0qv*Q0~qIH(Sbh z*x&jWOo`q9uIRTQ#Ttm78=5jGNGD}yTLKn^!)qwZ^HQDw?a+1*6$vRtDJ3NpF$pnI zNog=xT1-q)MMX+lMieX~t|%e)FUNnwDoHD{i_#E(fbcp{C~wlpl&E6-r3E_+1cse4A6IU#yh(^I=k>d4JCQ* znxJuZ&Ytc9zlY~MW$@t%yNim0fJ3|L%7MH~!P5>+h$&HQ{~K%fe~B50Vhreaa{P~E`L~ObfxmD6llqj#euvcj)D;haM7gIg)#>OpOG$9ZV2fwwFy&j*B*MvAOUG;@lT)sr5_9EyzrzW64 zlU+rHmG;F$qe68pDyG}&&H@jD9$ah?M>Fs|o;eEm(JU(?WzMtup-Exda;?#=e{tIV z$MW&&^zubp`(I_3_Id-K1LHRnA?lf0jJYL4$nNu&-==v56j%9nxLde3(pt^_nmNve z_#b{ZHh>D43O--I{A|mOrEjm%apW-f*&+X-3^cCrZdB~KTt`5wYFeDT36s^PL)5)G z&DJl*#J}|Qx0kcBvcfOEJ?*y9UP$>uPDH1*8y_;4xks(9uSWzNuH@tLb<+jo*R6_` zN_(%p`6#pXboShJT6R=Cl%C~Uo#kb7|J?# z2QE4m>-sAZ^u~2IMcmgLCt2`GNZ-O3Qu@eV?S=`VrUbl^PU`GHSZd3rva?|{z-0(G zZHs&v6N#ELBZTUN&kB`$J603JU^FSvl*PL9nOie7#ZY94Bg6I+YdgE#SwXqusiWQ( zx76nFgNE%4Ufk)m-9Kv6A2#0Lwx0+bDY*j7@i0J;W6<**Ym-URTHyt@;o!1!hZ0mH z3`Glc2dU-NtFlD)MC(Sws`b$dTaV)dS6SD&$A$-o`AJ;vO#OS2|LD24KA5fM>*^MG z6Mx*sDl6?*vC-iE65Ckpeg$}+X~Y2Lv>Jco<9Kszjb9VCu{;~w`Z8|OYM|ZT;IDl@6+Npa#n!IRe6_`3oA~(KII2u2195x=~ zwsDK*W3h`Skz0>k!^TL0w@>Pj-0-m?+~BvwDBbGZ8@eQwoorzdkLS75pL^|pRV3t^ z4-EwOM;2TU1yvkR{zBW1o#c(Tr~6V#pm6z9jh?lrzE=f_k!MS174&DDlyGOt&Cwr z{6nhI>AB0rwUNinB>PT5#C^fR7H6q9OY)N+4S*P)?aQ>5vebS7xj>=>Z|CmruCRi_ zjK}(@t7f2;?Iz&h6*%=t$UukBd^wfy6?mv7Ojn!zMDVB;E*%H!a~+rwtj!@p$R z_T+Gnn4G*4e=|2fe4Z%`**mr@xL^C6~uNkeS-)$ap~B}>h8+QN~KwrfSQp0(Uxv8PiN9+EtApM z(|K&4)r6UWOsvIOubTT^`+%7y?|Z|RMebI-F!~bmWudt?tsrNrl{-&afTLf7z9k@Q zg;e!6WU^w2SU<2wvHbpel}G=S=S0YPIqMjcBKsIv$;15_&rIXMFJ~~HD|7wDU0&wq z5Fp- zt0J#yzEx@km3@7fA9IoulZfTxn6@Ho1&Dgy{W8mJu_=P`+L!uFAf*@(d$K-32P2x1r3F z)ho-i9AB9YUR)%e*_)417@yv%4B!xbzBXK1)8c<*R+1u-Zdq(OmEnDeUY_p1|1FYj z>vNgajA>?FhP*GS#&LR5aLOaK#%b^ktkfKxDv6W3v%SzAH`SYZ&mr3X*UA2-?BmC= zCNG5jtMb^m;Y982AjJZQ8hQ5~mSSzRtE=meT0!IW+{;$g*X4aim#N=egaaa?ZzxQw zMqfd0HGPZz?0-V=J$Mzi-hC&dTyJ63Zz<&b{qi2qNn{D4rgBj$aolXGsfub|#4u00 zVqw9~0oB*^Gi+`E| zn?3ulr{-vgW5++?3gCmL7chj)PumPQ6G0+wvvwd@@=}LLX07XE0=Fr)9@KB$GZ7>6Ek1&pXuEw`aI!| zZnFgE)~U{0;o>qEw^*81i>y%+W7oxVB_cMB7p`Ds~a;J4{D1I<_aMR>n z0FlPc_f>6TUT%99{eXIDi|%GT@g8cZTq z&)nN`)*nJ2)nTZ2e`S*4a`srA3EQ23g0e}1aO4^ZGckOKS=^{@N_hjKJZn^?QN=wNi5LP8jYYyl3M{f=0QSAFTS*ohD9%e3~2 z)6EDesdn-DOX(k(N36Cuqp#K9OXi9Q{4wSrsWj&O)P~3vgfS%I8uwsayWMvrp_$(5 zJGys;4J&Wvh`kqTX%ZUh)kJt)Wk=3932p0HIPpe= zp7C0Ln~fKBY1G8r@L*8=b=axd)nACI2R&>seqwVjJBy)s?}c(3JhT2pt#gcEtm%qseOF9TM{@Fh2TlS`f zI&oxb@>EyI^vX!TY08zTn3&pn>THe~5e&;!v5t4IkM;8ov@&&wx(2&*_nLiv;2FT2 zqPL^;cU-mQ`4Mr)?+@#0Cw*%3Oqk38pT;`oAfBtS?$jUCtW|8TJb)qe-ut8ZJ%q@u zmif_ZO>}~!WS$yQ-YRI*93(k>A`82-!n!~IS>E`?Ubqg9V^fCQ)vffy!M;*iCZqz&WGxAg#@>G{t=Dgt9Djb%2I%4@M3#w-J(qZU|p> zZeHXVF&fY{DCH+}PrQ#x6-UqQQVrA5fxeMD9otU%=Y~JoPNpoJ5htql4BsJ)T@X#a z^13QKN%n?l#2M0~D?-LT?;GlP>w}e5Ol_=pUb$KIoPjOf! zzOXI;h`!6zr`vDOZ?N5lG>8#1>m=BNQt8AqvO~k<^KE9S&vWSDHDqRl1r3=D9d7t+ zD;5DGHcW~%Z51!8IOYguoci=i;CSIjFw`z<3ZH4R3SGJZmk5MY>Y-iuA(OuZ&xa^8 zAIVuN8oE)n8~zKEDbF5wzv-!y-1)E$CMz{U!ogZgiqgpo`3yRVIt!q)wC z$CQN(A{1A$!_mVQ!GPl-(wWuUl;xQ+r7@J#MDUhKI?d+*wiBp<(qA5&ydPW^JXhKb zQ*v6~l_z@E-O0`r`>yj0Z60@_VL!vS7%@@`WMn(?qGrqCh{G!A19>jJF7h&^-JWA7 zx_L9WP~PVjo7=A1-n??~JMOwc#wo)X&YRW=Im)`LZCOtbYmvu~F3ytAG;O`6*`X<- z>TWARUlkBKqZR{sy;j!PSAQoY^?{YV9#GfmTMBmsi=c5#!Od5pm8#Jth!KKDd^XQ^ zp|_Us;?PCwl*>UOu8{~fatn}A+BA`cij11Rt*g#5sI2imV>WnVl(fseKDr)-+ih>R zpUKx$ho|sWLoGN(ZjRMG8E$1V(`^vt_@zx(yYVqUhFrK=W6oV9m-F}?M(OQa$SgAk zJ64V+nzvYjMZW|mbPpG|yJB9tV%w;6wY27OVL^l;2N|&SU<|p5w}@}{vxPNmJ=Ch!q1P8avEscNE_gHyQeOSf6!mRQ1DlFD29czC@yPJ8^(ad zge^VZ6mqoi0F6~z=pRAbcg5!jsR3yJwa-O~Rh2Qetw}GHE4Y+!`Xk1MhD=mNp{KB# zO*q(GpyFxYhxgS{AQomseLc3Zsp=%Yo*aCrsL)#&ab;RfQzW=|##*TLv(FfIFf>Ccvc9?gNZ8lG4%63e z^XQO8{F{D0!C)Zq;j^38e2kz@CGm5)Dp{@*LJU>hqOiUww4HAubI7;I!9ru9#wAc- z1#b04Jr(>%1E!2n+69#vSDsPQHBowXy{d%f+8gGTHopxN^!_d95R{X-gZtL%!*DQ2 zc8z+nETD<7H9eJ{>eL&1vG!dL-m;@Y?)~22>nm{ZWU_XzN>(^TUChkN-(|E1D+0<( z-f)&PwE|rg=fRtHTv*QT(DxmSe@ipHI`|g%snQj|7?@CUr^d#|V9i02YFo54#Kcv{ zc?=(HzkouRMeV0YJ$TGj68`q?gbbRd?Hz(crr~A6}Z|i4OwGYkMEHX4xc#JMP)^E9|F{2lzl@ zO*F!}-`9o7?=3NmmUN_!${F*MP(t{cHD!IxquCTF+X;_&ayEit(#-$62I}P3Cp>TP zi{dnOShNR{|NRdE#GQyRy_!j(X(nhFLqo-evn2RCTvW~rsN#K-MdRtZLREPZk0)xt zOwu5Wm-Ov3{2+DpQb2X=i&X52Y4Rig{`SLSxsy5gn(3Xsk~#=Fe9U++P5MW8#3`4M zVT44OxJ79am>LfBx*P!juFiKeT00;`@}_V~nPtLTZ&;A~bPcaIym58u<5!QTOdSDT z&z|y6pO*{&*03?My=F67969i|>ZcZV6Dm)O$i5<3p;Wx*Uy(bpy4R-2$OZwhb$#+! zVYL84L}L?{FKPkkRA^>{y`!OpnKM%Dy8#n}ibu+%`Tph&uZPjPgdgp|r{}q6ZiQJx z$qLqq%$dllUyVU_*Ge6Xxb4D~gVqJI?wd_ygyIg$t>-?lE zPspwwvkRt&ogv#*@ky{?*E{%y`4OAur65Hr za*J4-I14;kI7x1n5%G)umn}y??%OhI&e6?A`mvoS2z<|bR;skp!-m@v3F(Wral(5` zx|ORDUm#qM(hrV&hUXWTaOiNka!-dDFMu{v*hDud2=z6#^O^K^Yy#-7;pq=KK}_eY zCsTUC#oB2D9J*XGeK3<=<(4I6|%aJ6%|UWn&l zPFNU!QYZG@vTf*+HOeNL^!$uuZWwPSY>kEtqQ|TAi3fbG{1)2cA&yMY<>tifFEq?} z32(~w+5aj)DRZc5EUu2foZy1Hi*Uc7f7j4HdfW6%UkAv7_#g)JgrJx7nc1Qg&PuNc zrkzsS^yl=X`$VRx;`(aAF4BFaxuAQG) zh6xcGu7y=ZO|=l|>4>Au4Pr6Y%>-A}aMRG8FKD&Ds|W3`fR1%xza5w#@2CoU*DSys zA43J|9D5ds;RG&!s{`!qw$+g++@?gZBERN(RI^l{SgE7uJ7W6$7p?lxc{i!FN{*Ji@j9(VfqSL zZE9euhy8qLKB4bV$!XObI&hiX8P+|nFv#g6L@yHM8`qX}myLf6@F4Ft!C4P++HNh{ehjN-Q1?9y-y!8zxs)?R_6iPeBt5J9YcIP zFCBRS%1ul1r&0Yc{Ks&{!(CZIw1@FP!aKbXve@6?*jKsJfdUnKtbU=hdIvQU6C!ve zJ6N&Kd{FsI=hL5_DFHB<$_0TZZ#nilGWq%Vwlg6CtgmnUDRl*GTsZEm^wXINy1Cl7 zL^hcC{DV7IS6u}Hj7^P2o!3hiLnY3N#YggY7wW`^mwggA?mrA<7rymK$m_x<1+*8y u`e4uJ%*vy3uU_9@qWTJ&b;3)5ROFG3=NzpU=YRhe(NNJ*IA^#1^9DNIlR literal 0 HcmV?d00001 diff --git a/apps/privacy-panel/style/images/default.png b/apps/privacy-panel/style/images/default.png new file mode 100644 index 0000000000000000000000000000000000000000..140a03ad9046f9b03bf57ab4a09c58a53eb3f54a GIT binary patch literal 1343 zcmV-F1;F}=P)MGiCDls~Jjx`;b+QbFFC)kUh7{52oA2|vkW5>Y2fQW42Z7X~ZcVGzS zAr2Whwq&fad27ubYfr1xeW{M@g0CS8MW72RLex5|tI>ivv>Uu|0;Jdpc%l2UgE_PZ z!ayHL7<%M`D?>ZsA#pHwi1UV(3c8q9ZaMP)F@UIh)PL51NX z6~P?a0SiD4ZV^7ICzxZ~t%Ha)N%Y4T3Fg=q2m&>kFtl7G_1#|38ASsDdnnIBX1$3S9 zHIGS0e|BbYrtnq=l90mN*qDDtT39c~V?4)er07+H(}g#m0!dj}-ykg%t_pL!MpAdR zU@wq_q-j#XFvuSYRfRcTBN^zO>040?6k{!!sBkOY2%`06S(oQY2StLUOq$h zeQ~Q}*DM?XsS@8!(zriBo~fC-;0!*C&r~VzNZ>-1jYN{XK8;2uriw40|U;;A~a49Xq*v3`m8? zV-q59wkk2ltK5+jNQI$MuLzv2ij#xy0;%Bij7d-#-0ygqJ?aHgVQ^$r1kP5a7r}>Z zPz0*ipxaZGR}0aU5j}GrbG+uvc@_6&HC`+I0$@9P7y+u+uWnC;Q7uUERF<|fCO!W% zPO%qj<#>$ecnz@7cw$Lqyy9dM^&x3b3+D>j&A2`9aS(O@t!$>rpMtV-@T0jp8%x^DtJg)4!%=@wC|kB*wCklafcc#+YQRo11C{Uxyr!gr%JX|2ZyM zdX5b=wW&ERnQvzZQQQ%@nMd>0zWoj>`$QZ@!`twJ}?^~)C8di#4Dpa z;n5|=Iziwg0JXz!fbjuC4Y+aHALHThZWxCXfyfi5)lCj3-tu_7Do;Iob`UjpCS(B3z_=o@i002ovPDHLkV1oCU Bfd&8o literal 0 HcmV?d00001 diff --git a/apps/privacy-panel/style/images/default@1.5x.png b/apps/privacy-panel/style/images/default@1.5x.png new file mode 100644 index 0000000000000000000000000000000000000000..8477d52ac3317fc41258669430130dcd1ffaa37e GIT binary patch literal 2023 zcmVOG3Hc{RrQS8#+LDWw1!eI>a(Bg=eEExNxz%jVH@CmI0tUX z1C>c0+;C36Mnf%y=D1Gy6Z{k6K#IimYyKIW)f$=uH$gi@fNU0lc4(@W&>XlKzJd&p zFd6s?n%~sW9QbYM01Zeu4bDT8?9k?Wa2sp@y4V;ogbL8VN2=s;BcFtApc)f{ zlPW@UL>hN`&Uf@4!kuvLVV%${Cs?H-fJo*Te^n&oh5E_s`Sw2&n{B+n1@Oo_Swi`pTZ3QNfO&lGl{VW!1+@`w*g6! z)bbPzuh7WYIOCsrO8Gon+xwG3*|*gkwCzvr=f#e3Z~%14%LYbe4l22s1n@N#}u~ceH#H zWPv0IKYLEYF87h}EY6W43rz=wZU>Sep2?BR>!Tq=JPMx0Ig(=k%`!9&I3WTg!O~h3 z0bM0oT3wSAIs#6s(5*lcB(fTfdb~6^>M9AJgL5UtZZ(BYu9J&Qag1~YZk%mmXPiARi)=%zGjxAos2s4@H18?JX%77bkli-ZIqDx8Mx?>Az+Swkl+a&r=&L|> z3xp%o_js_Zu&0#JR~=g!%>!hoxkBWDioo7{4ZC?9n;9((WEX#Uh3-CjTv2=tT+2=& z4xO+y^25hU*c?0I*i36VAk}6=E7aTnNGZN1u5E{`V*@{^HoI&@R*tW2$M38>^g?u< zdio#QYJT;Z`p~b^xg3xx(~HY=`@ut7P0j`RwWyTk&!0`g6hx-$IEms z45Z3baEWg9J+RdTLgTfl6ynex)uFrZJ+PI}uRQd)m5GH#x_-CMRv!NxzZR7~<}h~siz=_7k^gg+W4k1VzS?`Qlv$~vUEEj?o%sw%1#cio zS9R_$Kq`0w3-mpH*@Ixlm|uFiMZ;4wG&DJF z3CDP*g#DF={?G{hER|`1I8ddHBgA*N?-YgO6~D&llK{`D5QhdMG>axPT|kvK=0}9? zUQ0M;dCtQ6tAa=@X{@bm?-=W)UT1X1&oe4?>3#QwZdDcfa(AyuvB*oWwvCzPjo6Rz zUu+obm?`O6BkDr8nnJH7Gfr3qs-!XhJpH%3XCJH;ou3J>6=p8q>fN^ zH)3OEpJ9Z?u}X`8(-c~d{RvPdttNIU982zV(7Z1gqHk~Z6s*Kk#`^MR+*rrVL3^04 z7r|>#5W1b477N`3Ss*FA^Go#YjoWNr&@q3|Sobb0@tP!AXet_-t?s1129knr3;(`x ztKj=PeYAx8bnxXieb;l>SjP;{O47+K4-4H48ju9bTRZfho40?A-P=e1+Y=ebct%oW z4VtY(n`3tXNzpqzO5a@XwuECmBS{B0JZk8-VG~G#r>pBlL*tmF$Xnd*kkF>sXMv=+ z@AjA?^BRsxa+ceb8rlg1KoW#vNxJ^v5nZ}=lkv~ICMog&I7<$#$36}zN&hh3ANMh7 z{{W_-)UWz6#Xbc;0M%LIHh(S0*RgTz3qW<2E^woAg>D32J*tTh8p{@1kNplr>v0V) zpj=;>X^QemL_Z|L90Z6NufWTxOva31nN!sX#S2!71o;lZ1s6LLVp#klw^R{XOlfMhRj|0!gg(cwKykAzFKB z@3p>_gamHh7hilK+S)1FOnYAoX=snNj0v)p6(4JUrhq>1!t~ zghKcgd=5||AOvKf5L|*+Y#f;hA|F160U$vJ)OF?CIMR>eFnk8{K!VMy>pX1ZNFK#O z_!NRb&yZzx-G^)(Tcg+yryv0I3=6<1*l*(iEW~Tz28yxV@Ve|coTGRiF2YlwEv%<- z5uTS9hjSS3z#LFg)*QSeJ`VMQ^DEc{N}08(*6pCgI5>=-!6=ZmV_rBRDGm&O})Py)VjF(^# z$jX^PcuD6tIE>>k4rJxrI2_k84i2NxhM`3ug*wH-VY~<)pgm^|!HZhN!5=e2FOW8@ z-b}oknu+%@DuA?OeGB{cGLCaV+Op2&J`VMkxc2XSTAT1uPUGMYe;^E`Ju3nyauNr> z&@t}QRm%$pa}mdvK*q7Y%xWC`8%f5#&agJ&-K@l+e&=%*$XM1aJfDp?E&@pu425ab zH$i;^ZvM*OOAh2>M&rPQuSPAZZp?L)3BqK0@f+7T>=3WKC)ypX@4*ReY!h0U$|M);Fl9 zzn@wVKr%h9ejd-qNfUrWyNTmdAZdnu6V%etnX=*W2|iAuGdqd{2OfbSkR-E9LEiAR zJzfk-j3Wp~b`r;@5}!`G`&>JU1II~ydO4F;98WgB&xd&+NyexA)Y9IuV>q7YktP87 z>BMmoNRnVULTz2$yNUzH^E?s*Ihjrz{Xmio`zEOw8@yYXM`9fAl;Xg^SAZm042Ai} zQfzMfu3;XYPhucHPbH2^K$493CU|2r2J=ab<1dbJU_b$cWxdvj62BeZ+}55kn2+a` z7DpHgoZ@(2-cKmg)PxO9hJRfB&h6o^DUBa{IL-4*^ZvGRtgL@u3M58!gW9_9Q)6qJ zqn6Gt{&bd)!Pgc3KIt)-m*?kWBq>c82W~h3Q6MR%=K^Xoo$Yc zqX9Lrf$4XO_D2@^!DHcKzYG`eD+}y^t4q8|ZCq8~H zOik^bj>ltu9w#l1l0KgiRnR zdivdto7w<@)sW*HfweFm9oih{>FRUwSZNC5<0uA_f*Ww@7f5NYsrkjEb9txyJXVqm@o_W*Nzv=}P;FDow)jzH z!vC0td=L1MfqDi8soT|09X;Ri(*)gjx$(#{OrG&c8u!m&e4FEZxIbxeG{@ptif#mf zq-bbsr<$hboZj_slj(YRMn8L<1Y6IsZA&&aIg~)~r$W+7Fi0b-A?eKY-yspsCaZH;W z#+~{GU6~$;KW`zt5dt#J(Bu?V)ir33&(p;9g^-bPc(CA=Sa?$)5HsTlgg1TwG6ceS zNR_qq#^In%T4#8gXT#Wz4J2O=nUZevZQhq&puLwH%9UI-fal^ykfm*Xw~ar{btF9$$wE z>kaiB$7LYhjbl-7Ro57A+?+VB=FNuI&jRUg42xQZ#WfvYhY9NqXE~13X2da1H>>X% z57%Kz9H;XhhoZj#(w+D59NoBc*LZv#CagES#c>?7aU@{lIL3ds!hr;VbT*1by90SF}G1=7`j;;5+Fk=u2RG!a;$+K%oW&jEZq&%^WW zbPg3qL}y;^;iqwwh>Jso@nCU<=OF)+Ftzs&^8bRR;KAu-<9Dmvp z$IM!!2uN2WSk!N^xalaz22L)nY?*6xcAf?%r&5LSfh}`jUYj3@j5F?6vqKr z*E){dRVgd4jfIgln4j+_jq}nzjwl@15{C+;6i7$ISk$Xn+@u`B$YLNVpWCqBaU}j1 zjG2Em@;;D`hW&`6yeg?%*r*9?nA2R|*+uG~m|}R^@Nc<&`bju;J7D~R<9rh;kW|i( zuLH;E>I0`Z3Lva!9J6%wR;3e!(cm10QrFoNJEpp&J>i&g%%#Tgc;`7)AnF)LxDH*z zh(N)%ai~DP2GY$C7WE1iH?h>iU}ZJ+vEy3%JqgD&boa%^V0`jwWM*VKn8$xGKJRxlfl)5rdGl!&+`D|FPBxsw7NVA$JKX#7e9`VOZ{9|9L^Qj zpeF+|8Q$_;Gxkx=o~$DaV1j^C18v4L|IEQ+E4D zMrRj(G{!yk=%-_F+%n%LQ2_E&jza}<21pz{?>Y_@$Q!T;B+1>5ZpSe0;dX&xY{DD7uNjF$ z1#$^Ul8L1!RC>2IVHnq{Yx#DGaa`j4%yb+tz#Nby{9^M;MHT(+dRfdB{=JLG^Af|D zgBLO$hYI9f*c2DY>;hfCS4SMijhZ_CUh31yyS$HCi9-eQ6_7L!SN>kn%aKH~k#X&fq$^FWri&htL) zRUG@E3dqvVHQ1M%IGjJ6>jkp3(d+P`TJGXdfgFV)pgm^|!%^NBE#gpt6oL+k`*F-p4Q^4(GSpqd?ZKMd3HG^))>X70Ao>)7)Jf zgO_KasNg8G?TVXxEOoaQ$QR}k-V4kuN+!)@Lp{Fq{ZRzJE@l(A4Tk=#n-Xl z`4k(6b0lwq8_0@r! zYhmMXek1JzD1~((jjcl|eBk_dBQ}m~eezobB`~JZTihj3l@pin9i*>h=MsQ^#FU^Zb(MmA-cW Y2E}T=njs1(C;$Ke07*qoM6N<$g8iJ(@c;k- literal 0 HcmV?d00001 diff --git a/apps/privacy-panel/style/images/default@2x.png b/apps/privacy-panel/style/images/default@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5d8c5b208b402e92e380f7051c358e91b37f3ba4 GIT binary patch literal 2616 zcmV-83di+{P)&cOqS0u@ON8qK|YrUUOTY8h063$}n1vjr}whB6&E z^RRz{8Zdx_FrWsG>%hsQmO>?XfQ0ivC6w;>z#R4?sDw$NJ!TRr;iLT+nB$&+QJ_7} z2PgJRU=Di}>VWo{`*2i#VEgykA0P^}PetJ(lFY z49V1CKrM0fTVi6wodNh5i_oZU*l`iBiHRFFvtBfkW?#?7(E{GBIrO;xW;3o zNB%miz-#!xHu!*~8ud?-3nA@b43Cw}$E~snTnQxA^vo=|`Uf(CvC<=--cevoD1}KN ziTJ&>$L+~DFptF?$$_V!bSHsN0ZHYZ@N>8w!5qnf*{O^JuNoU}AcX(z%~}Y zudYN5b38=({n~3g2Wv_V%+4nZ+z2GiQY22@eFN0#>P_j*!cxM#*|*EKb9haxEj4hX z71)gXF+_o+nVt(GNKZDkO|+BE`iW^LENJ$Rf%joSmy14%P7F-aZW&XlI6U(`IP^>4rI>LDNTRO30c!7Z=2cg3Ki^BLLmapQNSgVT zRcb?kLYP}#l^VE$16Kn{^YqOu4!^q~a4)HWFL2;1K$0v*;&?yxQrn~Mg5Z1Mo)QE9 zyX0-uxC0~u$BmJDWRzNQ@P#yhd-A=p2T5uJ|Qr!X$ zmJhh4v#Y53u?Osh_auRz1BWEU?H~41^TS6fz+S{eUMN|N80$c)3a`hgcgRakI9Qcn zPrNtYqsrDfa7_8Q9i1wY5ZNeMz!xeCaoj;OuEKbayjNAvQQ5#fIJia}paj^fx`D%y zcpOMI4ebv_!JbuQaNzYl8(0Tk=fGhl1GjdFqG;d+4m=K|ntB|XB#H+1mxLnmZXmUA z;C4~4XB9b1g6r`+K&rXl(k=@2tfD&{_zI9}>RQ@FQ8e(ClKJ&mHIQoVHMfd_J*%ji z16S@2&9r)N3$pj25z(h&%KXb z0#eCOP0gZU&#L*?_Q2)J1#WcgSY1my)pvAeHJ4#7*67G;u7ZKfw*`)V3^5=@WEJ;m zaDk@Zu8^b4wd46sn1i{z2CtPx;HXMsaKH*|Ms5UB#T^`4rtX<>c^;ctjZpuXpW1r| zGScUp;5pWOti@|)z6bRJfA~Luv)T92Y9LkIZg6DeZgV?LAxN(8k2QJijQ4>#D!Gs> z@L|{jQo$`8S~|7%adQ$G_ej4NwF3XlEfWH>nYE}BNCmeZG})?kw9%9Kg_QH3FD}#Q zi`R_&Gr@%GlmAI9li(dm3BWEs|pvlOcnS%kP7~TLrdkSG;vv<&u@+I9rY(% z?;7-4*D;n~vyF^<+U`lAz!&TSmw};1;KpQd(5*P|aN7B8eQs;c@Uxc**FBQ~>pI4! zU61dHd#Bu&B5@7)b;`i(&06#ZP_Z?;`jY79gU)DvLuWVL#KE)C>6wJlx+yDc4b^F9KC=Sd*pdauuKUB_7Sb@7g;4Qxh!3?x}C zqWy??%;8M@4twyh+qz!6Hv(t&4}!DTtC3SclGP(%OTlq;lRreBC?wM z4GFKYp}@5Cz^A#{P6K}gJ|Kx+EUnVb1_uYU#bcghtl+plZeH56~ zwlGF=TvoxY>;v0Jt^<#}Opn=WE-@>MjkT;v~EuVE!0-KSmU`xly zxh<&TGcIaiGxGO2kF4XK;j>jDu=Sm+|G3VDP53LHsZxQ>$R~I8U_y(*Nj^&@1Dlb* zgs_gaV`2CspP^ELZ5}atb+n9)T95uD1U4fdgzG@IwCmPys6+%#41G4~Qz$J0XRWi6 z64>TZqen-zc&tZ$@&j9uOX1=^|66tpE?S>q78}?;^ykp9592n#Z>+PBA2@C36X4qi zVaMQv-C4*FZ2#f=VCtwsJZD+aCRUJl#uTL;cI_5mn| zOW?@!XTyTvFz;0k2W7x*CrYp?-i+Av=Wn`2z&oXh4K6>x~x amhOLYn!J}iF}m3R0000NDHX| literal 0 HcmV?d00001 diff --git a/apps/privacy-panel/style/images/guided_tour/gt-pager.png b/apps/privacy-panel/style/images/guided_tour/gt-pager.png new file mode 100755 index 0000000000000000000000000000000000000000..e8c67f3d8c33a572040ccb530b578d1d80123412 GIT binary patch literal 1182 zcmeAS@N?(olHy`uVBq!ia0vp^`+zuwgAGVpT|gW!U_%O?XxI14-? ziy0WWg+Z8+Vb&Z8pdfpRr>`sfJuV3@11S-|Uxf?|EZUwfjv*Dd-rltDjtP}$_^56^ zP1@t>T_^iPFHJOcg>D?1uzu1Dv8b+D36oi*_ctuJXQzO;S7_(AZ4(1%Yyeg&TUDSK_s#cG~A4C{KX^FAoH-7B@{ zG&?){_J-XDzD~K+Uwi+*Wt=_#;SYu%9vblM|5p3|^u2rc4juOM^Gj>LCsl9K{cLNZ z#Jm?T-(GrsO#km~&87Cw%!9o4Gv_nruld-#d-tKfdH4MG@>Pib_%vtf`iAgp7C&xr zSyxqAf88az(R+%l?cTXnvL9G>RGzzZwlMwb&%{=!kid@l#ruBA|L#s`9bqT?zx|3=PLD|*Qh?Pk^aH=LuKigimSoy@0*Lm?p?opdu#d64<9D% zd%s_+8xyWi#? z&Q~zn$lkwQ^5x5_uji)SzV!5r(3 znz8qWxU2J?hUqT>DmtBZw~>{=5hn z0`pXN80u$%EtmcbHfROdg?VSXqd;zcaBR=HP2op2vzR|~)dQMzS~ub5W$q`DdAbLu z-DXYJyt6NzbKZ^3cdh|VRR5;4y|ilDgHGGiYvVl57ez0}E~k2Kp`W@ZIywd&ASPeBt#!g>rUY<$ehaWwVd7^pWI{l>^O3(Jr-T?eqko z+j;L@0~w>gBz@=A)dz1^fh@TCAa^g&wO^fpRKWO literal 0 HcmV?d00001 diff --git a/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-01.svg b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-01.svg new file mode 100644 index 000000000000..50049bfa9467 --- /dev/null +++ b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-01.svg @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-02.svg b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-02.svg new file mode 100644 index 000000000000..13bef7da205e --- /dev/null +++ b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-02.svg @@ -0,0 +1,493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-03.svg b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-03.svg new file mode 100644 index 000000000000..8b2966ffef2f --- /dev/null +++ b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-03.svg @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-04.svg b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-04.svg new file mode 100644 index 000000000000..dbb999d3200f --- /dev/null +++ b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-04.svg @@ -0,0 +1,535 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-05.svg b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-05.svg new file mode 100644 index 000000000000..3226a28c2a06 --- /dev/null +++ b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-05.svg @@ -0,0 +1,402 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-06.svg b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-06.svg new file mode 100644 index 000000000000..49a08c38ee0f --- /dev/null +++ b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-06.svg @@ -0,0 +1,463 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-07.svg b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-07.svg new file mode 100644 index 000000000000..3192326d5c17 --- /dev/null +++ b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-07.svg @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-08.svg b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-08.svg new file mode 100644 index 000000000000..bff94808c80e --- /dev/null +++ b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-08.svg @@ -0,0 +1,355 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-09.svg b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-09.svg new file mode 100644 index 000000000000..e68aa05034d8 --- /dev/null +++ b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-09.svg @@ -0,0 +1,493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-10.svg b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-10.svg new file mode 100644 index 000000000000..5b586a3beabe --- /dev/null +++ b/apps/privacy-panel/style/images/guided_tour/privacy-panel-gt-10.svg @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/privacy-panel/style/images/range_thumb.png b/apps/privacy-panel/style/images/range_thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..246b7b4ca3fd9a283a1a3234240f1afa4dda11b6 GIT binary patch literal 636 zcmXAlYiN>T6vy9XfsGtGMq6Xm)`h@gVGtCuB&V5FW7%lzOKZ^uXA#93n|-P%>ci;5 zJ~SHD2boLIa{AJ+CQNhQZ+V&bdovU_O?#b7vJ>xt=l>j@=bYd9KhLw?9uBF#wH`qb z68AKV2YcK9MI?Z-U5957q^_y=Y#+N;t7WsguXQ zA}JIKpi-$6&f!+AR!1U{bUKZqC@9_zs?}-(fdCZX2^0emu+R$%5IE86^*A3AkH-Ui zz(RllAmBqS77J8@I-O2@qDG?urcS4WE`z~fG#X7Nli6&xTCFyl&2G0l91bXUyWPO_ zdc8iM&+qqxnP4y&3WdVqFc^+Tqp?^lkx0P1R#sNPc_x#|<#PFa9=sKcMGV6#mCC## zS%PmmHp1(vqkq&&B7Ziil<8UG?pCJmPFFWCHS$eVc~!?} z4Lm{oZ>};s&e3NEr)ez4^_#RChc3~~8qzh3Y^1wcfxaBrY57{!u6GWdI745`Y;kcK zwk5R6qZr4e9wIbNQz!Z4zf-*G$A)*b#m9dpYOdG);rE5fT?eVWCH{e*Eql>8CX$GA npSIIBSrZe@!UbkX@PJMyJkJcjo!Wf608d4@>>iekdHLZ#yf}$M literal 0 HcmV?d00001 diff --git a/apps/privacy-panel/style/images/range_thumb@1.5x.png b/apps/privacy-panel/style/images/range_thumb@1.5x.png new file mode 100644 index 0000000000000000000000000000000000000000..26a5092a221112769e6928b814481460c7dcb0bf GIT binary patch literal 960 zcmeAS@N?(olHy`uVBq!ia0vp^S|H593?x6vT4pjZF!l%dgt*S1KYzi31&bCfTC!xx zmoHzwef##~$B&;sfBygff9=|}uV24jwrts_PoGw;TJ`STyAK~etXQ!Er~s(t;K75} zuU`kUpFMj9)CknRY11Ykd&iC)4<0-KngcWwqVmX*BUi3mIez^3#fuk#A`k}9gqJU0 z0!@afId$sPxpU`$-1Y0%L)buBpw&Pwkc61Fd-v|Uckcp?-MV$_0!`tXZ>X&mN$s4j(=YbnwxmM~@vlcH+c| zlP6CC-E-#5nX_ll0$q6i{P_zPE?l~F>GI{vKu=x0diC11Yrt^0apT6#n>TOYzJ2G; z9iSuc-Ma@21fV0IJbCi;>C@-WpTBzb3K*Dg-n@YXChp%o*_WrET{8J(iQ4PpZBuIxFJb%cXjAe@S;W3TIz;Pc z$nEF-bG?rIagN%#B~8&T&)mRjO1L;=2`vIfX!abclNhM zf;}(dR;$c?tf738j;kNF@Y~RiO z3}sPMr26MExVwE6IJYTMr!3BBz3y)Q^Ah~KbuYC__U%b74Ycip+V z`pImbqP@F*q+D6oQvALnrebN>JAu&N_nYN~-uz1ueYVF`U+&V*^YWI@3N}WrbO~;( z%u=uLnV?cAb7!XE^g3q0LeITN@7>Seer5U-#+6JT*W2^F;hsFdCy-gX^fV~4Jzf1= J);T3K0RWciJp=#% literal 0 HcmV?d00001 diff --git a/apps/privacy-panel/style/images/range_thumb@2.25x.png b/apps/privacy-panel/style/images/range_thumb@2.25x.png new file mode 100644 index 0000000000000000000000000000000000000000..fb66057940318dd0a1a2ea1fcbf484e0b3247473 GIT binary patch literal 1389 zcmX9-3s6#N6ehFrF>2ecn^_A~X1R^!PC4!xqOOXr<(O%0y5(v!t*x26l{P*`*EHQ~ zdZ-nXnx?IaGAQ_dgo%QR3W!1kiV5NaLQw=2rBU`6XXc*&+%x|<=li~M@57Mb0Bg&= zmIwsGI`9|2P&mD|9`o(sqcf!{1j58DB%1T>X+sw=icqs4b=@}gzg>oX1*x1;pR4TPvE#&+9`h-Fuxflt?6< zot@wT!~Fa_fPpFX_4O?+EpoXWGB6r4#{N@NQ*YnC1>fJ_FAxZz8}u;}0oc$DSU>=s zApnjaKYrx%`EX}&a1a8xZ{!FWh}mrR$jAtIh@lonz#u?tRHL@G7A`|`0MpRW02F|4 zb8|BQYi(@>j(`NX19%#Z2D6MJ1MA-2Uf{@NG6CekzyOS9u~;yL!{H1K4RN{L;o)JJ z%j59?();)Cfd~j95{W>rv9U4G9AHmOOn@d*sT8!8$z(uzdU_hPot>QpSzra9K79g7 z=jP@>BG}`?!h&&kOG`_jAxI6n!A3v|APajB#A$HwWn4)N4Gx2I%S@0+6bgM9%;g6# z9M1oX1)K>Aiix>?J?;kBYp^n~H~&>sR#K@{U<~#S75ZV78Vx8&f*%K`5gs#X{29o z_%=J+U9p*we(eu-J$mGV<$kSHbW`SNs|mNw)>RzHe|I+Bx82q9@Ls!> zrX!3ZXG-K-{!&9{gkz~Ibt-dKcJZx8Jwa4Qg(Of~Yl85bBE-C0X=ia?zvT@{{ z#r+~-G5scT5_^#SuaY)Ct#Pn(Fanl?tb%UjO_``Gj>bHvx#6A z%tlZ#i@*3>q!TvliS|h3gPaeXAVHLToaT5izregW%OpGe(9y|pTAHWVTzg1cnYI|G zQS8p9zl=!pOhUg9pK^C^ub}VzUjLY2b4w7a9xr0a6Lc4SPQ-og#3@M}E9}jcTUTOt rd>hG_ielcU9<}7rTMa@37iZWf^zqz`zqseZ)*ljx3-%-XL}&dEoBT^a literal 0 HcmV?d00001 diff --git a/apps/privacy-panel/style/images/range_thumb@2x.png b/apps/privacy-panel/style/images/range_thumb@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5f7b21073bfe7b4bd3ee9df6b7341d6045121d06 GIT binary patch literal 996 zcmVW^dlV|9lfec`m+8&pVP%)hJ%{Wgl5wgw7kYZN7`@N zoF=}+aIj_TifE&*uj>qm2dHiJ(6R~aYl=l+_JaC zg)xIoTT^cGk_?-MY;!`BrqY~iioUUejhRNiflQh$!?ro5L8IERHAF)+#tSOT3MXXP zzLGS@C@AZSMG?w4!TP;AFUgX*vK>yTQ~$Lt>Z60T>6%hwA88^5YSh%4tu{JXU8r(I zc49B6QeCW$3d%NFmDyu=VtHibo;)`qSb12XM9vf?DpZ~h4VEosxy(KVQ|wWud|sX! z5-fdR;yHV!DN>^J!;oO{evv~COmj$);^QH~gXRBTGR*-8n?r(yVu9zBOjMwt4hiOZ zIsP$Ko*cQ^0m1B4mR-uG`kO4-n*)N22H4;&+7)aOAfN>70TB>^aknSG-LLpr zqr*Ex-mr!rzvjQ=7ky*zhrU}r*~cGL>5lD;C_a3$@3v1|Ft7o8Z+JJocuDY@_an90 z#ETd2y4UIzmkjOr9*bweqj~U<;+ziysbpBfgQx3xD?J}(g>MDTI(#MN~zx$d~cmlzH@FHAXq zbpGy?oLwg$^_-G((fN~e#wi9d9CZBOjAPz0@0fSYI>cy(gHI=PeL;I2oY=pH)7&J5 S;MflU0000 a, +ul li > span, +ul li > small, +ul li > label > span, +ul li > label > small { + text-decoration: none; + outline: 0; + color: #000; + -moz-box-sizing: border-box; + font-size: 1.9rem; + padding: 0 3rem; + margin: 0 -1.5rem; +} + +ul li, +ul li > a, +ul li > label { + min-height: 6rem; + display: flex; + flex-direction: column; + justify-content: center; +} + +ul li[hidden] { + display: none; +} + +ul li > a > span { + pointer-events: none; +} + +ul li.active a, +ul li.active span, +ul li:not([aria-disabled="true"]) > small + a:active, +ul li:not([aria-disabled="true"]) > a[href]:active, +ul li:not([aria-disabled="true"]) > small + a:focus, +ul li:not([aria-disabled="true"]) > a[href]:focus { + background-color: #b2f2ff; + color: #222; +} + +ul li[aria-disabled="true"] > a, +ul li[aria-disabled="true"] > p { + color: #797e80; + opacity: 0.6; + pointer-events: none; +} + +ul > li > progress { + display: inline-block; + margin: 0; + padding: 0; +} + +ul > li > progress + span { + display: inline-block; + vertical-align: middle; + margin: 0; + padding: 0 0 0 1rem; + color: #505b5b; + font-size: 1.4rem; + line-height: 6rem; +} + +/* description + value on the same line */ +ul li > a span:nth-of-type(2):not(.button) { + position: absolute; + right: 1.5rem; + color: #505859; + line-height: 6rem; + height: 6rem; + top: 0; +} + +/* text ellipsis */ +ul li > *, ul li > label > *, ul li > a > * { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* sublines */ +ul li > small, ul li > label > small , ul li > a > small { + display: block; + font-size: 1.4rem; + color: #505859; + /* click/tap events should be caught by the element below the */ + pointer-events: none; +} + +small.multiline > span { + display: block; +} + +ul li > label { + font-size: 1.9rem; + line-height: 1.9rem; + font-weight: 400; + margin: 0; + padding: 1.5rem 1.5rem 1rem; +} + +ul li > label ~ input { + margin-bottom: 1.5rem; +} + +ul li > label.pack-checkbox > small, +ul li > label.pack-radio > small { + right: 6.1rem; +} + +ul li > label.pack-switch > small { + right: 9rem; +} +/****************************************************************************** + * Boolean buttons + */ + +ul li button, +ul li a[role="button"] { + display: inline-block; + margin: 1rem 0; +} + +/****************************************************************************** + * Boolean inputs + */ + +/* custom styles for boolean inputs (see /shared/style/switches.css) */ +ul li label { + text-transform: none; +} + +ul li > label:not([for]), +ul li > label.pack-checkbox, +ul li > label.pack-radio, +ul li > label.pack-switch { + padding: 0 1.5rem; + margin: 0 -1.5rem; + width: 100%; + height: 100%; + min-height: 6rem; + overflow: visible; + display: flex; + flex-direction: column; + justify-content: center; +} + +ul li > label:not([for]) > span, +ul li > label.pack-checkbox > span, +ul li > label.pack-radio > span, +ul li > label.pack-switch > span { + line-height: normal; + height: auto; +} + +ul li > label.pack-checkbox > span:after, +ul li > label.pack-radio > span:after, +ul li > label.pack-switch > input ~ span:after { + left: auto; + right: 1.5rem; +} + +ul li > label.pack-checkbox > span { + padding-right: 6.1rem; +} + +ul li > label.pack-radio ~ p { + padding-right: 3rem; +} + +ul li > label.pack-switch > span { + padding-right: 9rem; +} + +ul li > label.pack-radio > span { + padding-right: 5.9rem; +} + +ul li > label.pack-checkbox:active, +ul li > label.pack-radio:active, +ul li > label.pack-switch:active { + background-color: #b2f2ff; + color: #222; +} + +/****************************************************************************** + * Split panel + */ +ul li.pack-split .name:active { + background-color: #B2F2FF; +} + +ul li.pack-split .name { + position: absolute; + top: 0; + width: calc(100% - 9rem); + padding-left: 1.5rem; +} + +ul li.pack-split:after { + content: ''; + position: absolute; + background-color: #E6E6E3; + right: 7.5rem; + width: 0.1rem; + height: calc(100% - 2rem); + top: 1rem; +} + +ul li.pack-split > span { + background-color: transparent; +} + +/****************************************************************************** + * Field inputs + */ + +ul li p { + font-size: 1.9rem; + line-height: 1.9rem; + font-weight: 400; + margin: 0; + padding: 1.5rem 1.5rem 1rem; +} + +ul li p + label:not([for]) { + top: 2.5rem; + height: calc(100% - 2.5rem); +} + + +/****************************************************************************** + * Range inputs + */ + +input[type=range] { + -moz-appearance: none; + border: none; + background: none; +} + +input[type=range]:-moz-focusring { + outline: none; +} + +ul li > label input[type="range"] { + height: 3rem; + width: calc(100% - 15rem); +} + +input[type=range]::-moz-range-track { + height: 0.1rem; + background-color: #7e7e7e; +} + +input[type=range]::-moz-range-progress { + height: 0.3rem; + background-color: #01c5ed; +} + +input[type=range]::-moz-range-thumb { + width: 2.8rem; + height: 2.8rem; + border: solid 0.1rem transparent; + border-radius: 3rem; + transition: border 0.15s ease; + -moz-box-sizing: border-box; + background: url(images/range_thumb.png) no-repeat 50% 50%; + background-size: 2.8rem 2.8rem; +} + +input[type=range]:active::-moz-range-thumb { + border: solid 0.4rem #01c5ed; +} + +/****************************************************************************** + * Progress, Meter + */ + +ul > li progress[value] { + display: block; + width: calc(100% - 3rem); + height: 3rem; + margin: 3rem auto; + background: #e7e7e7; + border: 0.1rem solid #b6b6b6; + border-radius: 0.3rem; +} + +ul > li progress[value]::-moz-progress-bar { + margin: 0.1rem; + height: calc(100% - 0.2rem); + border-radius: 0.2rem; + background-color: #82c72c; +} + + +/****************************************************************************** + * Definition lists + */ + +dl > * { + font-size: 1.7rem; +} + +dl dt { + padding-left: 3rem; + font-weight: 500; + border-bottom: 0.1rem solid black; +} + +dl dd { + margin: 0; + padding: 1rem 3rem; + border-bottom: 0.1rem solid #e6e6e3; /* same as "ul li" */ +} + + +/****************************************************************************** + * Right-to-Left layout + */ + +html[dir="rtl"] ul > li > progress + span { + padding-left: 0; + padding-right: 1rem; +} + +html[dir="rtl"] ul li > a span:nth-of-type(2):not(.button) { + left: 1.5rem; + right: auto; +} + +html[dir="rtl"] ul li > label.pack-checkbox > small, +html[dir="rtl"] ul li > label.pack-radio > small { + left: 6.1rem; + right: 4rem; +} + +html[dir="rtl"] ul li > label.pack-checkbox > small:not(.menu-item-desc), +html[dir="rtl"] ul li > label.pack-radio > small:not(.menu-item-desc) { + left: 9rem; + right: 2rem; +} + +html[dir="rtl"] ul li > label.pack-switch > small { + left: 9rem; + right: 4rem; +} + +html[dir="rtl"] ul li > label.pack-switch > small:not(.menu-item-desc) { + left: 9rem; + right: 2rem; +} + +html[dir="rtl"] ul li > label.pack-checkbox > span:after, +html[dir="rtl"] ul li > label.pack-radio > span:after, +html[dir="rtl"] ul li > label.pack-switch > input ~ span:after { + left: 1.5rem; + right: auto; +} + +html[dir="rtl"] ul li > label.pack-checkbox > span { + padding-left: 6.1rem; + +} +html[dir="rtl"] ul li > label.pack-checkbox > span:not(.menu-item) { + padding-left: 6.1rem; + padding-right: 3rem; +} + +html[dir="rtl"] ul li > label.pack-radio ~ p { + padding-left: 3rem; +} + +html[dir="rtl"] ul li > label.pack-switch > span { + padding-left: 9rem; +} + +html[dir="rtl"] ul li > label.pack-switch > span:not(.menu-item) { + padding-left: 9rem; + padding-right: 3rem; +} + +html[dir="rtl"] ul li > label.pack-radio > span { + padding-left: 5.9rem; +} + +html[dir="rtl"] ul li > label.pack-radio > span:not(.menu-item) { + padding-left: 5.9rem; + padding-right: 3rem; +} + +html[dir="rtl"] ul li > label input[type="range"] { + left: 7rem; + right: 5rem; +} + +html[dir="rtl"] dl dt { + padding-left: 0; + padding-right: 3rem; +} diff --git a/apps/privacy-panel/style/main.css b/apps/privacy-panel/style/main.css new file mode 100644 index 000000000000..e831dc03d156 --- /dev/null +++ b/apps/privacy-panel/style/main.css @@ -0,0 +1,195 @@ +/** + * Body style & layout + */ + +html, body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + font-size: 10px; + overflow: hidden; +} + +section[role="region"] { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; + overflow: hidden; +} + +section[role="region"] { + transform: translateX(+100%); +} + +section[role="region"].previous { + transform: translateX(-100%); +} + +section[role="region"].current { + transform: translateX(0); +} + +/** + * Only use the animation when ready + */ +body[data-ready="true"] section { + visibility: visible; +} + +body[data-ready="true"] section[role="region"] { + transition: transform .4s ease, visibility .4s; +} + +body[data-ready="true"] section[role="region"].current { + transition: transform .4s ease; +} + +/** + * Headers should not scroll with the rest of the page, except for #root. + */ +section[role="region"] > header { + position: absolute; +} + +section[role="region"] > div { + position: absolute; + top: 5rem; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: calc(100% - 5rem); + overflow-y: scroll; +} + +/* + * Need this for scrollable background layer + * optimization in gecko. See Bug 950250 + */ +section[role="region"], +section[role="region"] > div > ul { + background-color: #f4f4f4; +} + + +/** + * Explanation texts + */ + +.explanation { + padding: 0.5rem 3rem; + font-style: italic; + font-size: 1.3rem; + color: #505859; +} + +.description { + color: #505758; + font-size: 1.3rem; + line-height: 1.8rem; + white-space: normal; + -moz-hyphens: auto; +} + +.link-text { + font-size: 1.4rem; + color: #35679A; + text-decoration: underline; +} + +.link-text:active { + background-color: #c1d6e6; +} + +.hint label span { + top: 2.9rem; +} + +.hint span { + line-height: 6rem; +} + +.hint .explanation { + font-style: normal; + position: relative; + margin-top: -2rem; + padding: 0.5rem 4rem 1.3rem 1.5rem; + white-space: normal; +} + +.hint > label:not([for]) ~ .explanation { + padding-right: 8rem; +} + +.hint .explanation[hidden] { + display: none; +} + + +/** + * Disabled items + */ + +.disabled a, +.disabled p, +.disabled h2, +.disabled label, +.disabled select { + opacity: 0.6; + pointer-events: none; +} + +/** + * Headers + */ + +section[role="region"] > header:first-child h1 { + margin-right: 5rem; + margin-left: 5rem; + font-size: 2rem; +} + + +/** + * Right-To-Left layout + */ + +html[dir="rtl"] section[role="region"] { + transform: translateX(-100%); +} + +html[dir="rtl"] section[role="region"].previous { + transform: translateX(+100%); +} + +html[dir="rtl"] section[role="region"].current { + transform: translateX(0); +} + +/* 'show password' checkboxes */ +html[dir="rtl"] label[for^="pwd"] { + padding-left: inherit; + padding-right: 3rem; +} + +/* Following RTL Tweaks */ + +html[dir="rtl"] .hint .explanation { + padding-left: 4rem; + padding-right: 1.5rem; + white-space: normal; +} + +html[dir="rtl"] .hint > label:not([for]) ~ .explanation { + padding-left: 8rem; + padding-right: 1.5rem; +} + +html[dir="rtl"] ul[data-state="ready"] li > a { + padding-left: 6.2rem; /* 3rem (initial padding) + 3.2rem (wifi icon width) */ + padding-right: 3rem; +} diff --git a/apps/privacy-panel/style/menu.css b/apps/privacy-panel/style/menu.css new file mode 100644 index 000000000000..f8ad42fefaa9 --- /dev/null +++ b/apps/privacy-panel/style/menu.css @@ -0,0 +1,76 @@ + +/****************************************************************************** + * Setting icons + */ + +.menu-item { + position: static; + padding-left: 5.5rem; + background-repeat: no-repeat; + background-position: 1.4rem center; + background-size: 3rem 3rem; +} + +a.menu-item:after { + font-family: "gaia-icons"; + content: "forward"; + position: absolute; + top: 0; + right: 0.3rem; + font-size: 2.5rem; + line-height: 6rem; + color: #657073; +} + +.menu-item::before { + position: absolute; + left: 0; + top: 0; + width: 3rem; + height: 100%; + text-align: center; + line-height: 6rem; + color: #657073; +} + +label > .menu-item::before { + left: 1.5rem; +} + +label > .menu-item + small { + margin: 0 0 0 4rem; + padding: 0; +} + +.menu-item-desc { + left: 4rem; +} + +/****************************************************************************** + * Right-To-Left layout + */ +html[dir="rtl"] .menu-item { + padding-left: 0; + padding-right: 5.5rem; +} + +html[dir="rtl"] a.menu-item::after { + right: auto; + left: -0.5rem; + transform: rotate(180deg); +} + +html[dir="rtl"] .menu-item-desc { + left: auto; + right: 4rem; +} + +html[dir="rtl"] .menu-item::before { + left: auto; + right: calc((2.5rem - 2.8rem) / 2); +} + +html[dir="rtl"] label > .menu-item::before { + right: 1.5rem; + left: auto; +} diff --git a/apps/privacy-panel/style/panels.css b/apps/privacy-panel/style/panels.css new file mode 100644 index 000000000000..c9443429a718 --- /dev/null +++ b/apps/privacy-panel/style/panels.css @@ -0,0 +1,588 @@ +/** + * Global styles + */ + +:focus { + outline:none; +} + +::-moz-focus-inner { + border:0; +} + + +/** + * Root panel + */ + +section[data-settings="false"] #back-to-settings { + visibility: hidden; +} + +section[data-settings="true"] #back-to-settings { + visibility: visible; +} + + +/** + * Lists + */ + +ul li { + border-bottom: none; + border-top: 0.1rem solid #e6e6e3; +} + +ul li:first-child { + border-bottom: none; + border-top: none; +} + +input[type="range"].blur-slider { + width: calc(100% - 3rem); +} + +.link-text { + display: block; + padding: 1rem 0 2rem 0; +} + +.app-element img { + height: 3rem; + left: 0; + position: absolute; + top: 1.6rem; + width: 3rem; +} + +.app-info { + border-top: none; +} + +.app-info :after { + display: none; +} + +[hidden] { + visibility: hidden; +} + + +/** + * ALA + */ + +.show-when-geolocation-on { + display: none; +} + +section[data-geolocation="true"] .show-when-geolocation-on { + display: block; +} + +.show-when-ala-on, +.hide-when-ala-on { + display: none; +} + +section[data-geolocation="true"][data-ala="true"] .show-when-ala-on { + display: block; +} + +section[data-geolocation="true"][data-ala="false"] .hide-when-ala-on { + display: block; +} + +#ala-exception .type-box, +section[data-geolocation="true"][data-ala="true"] .type-box { + border-top: none; + display: none; +} + +#ala-exception[data-type="blur"] .type-box.type-blur, +section[data-geolocation="true"][data-ala="true"][data-type="blur"] .type-box.type-blur { + display: block; +} + +#ala-exception[data-type="user-defined"] .type-box.type-custom-location, +section[data-geolocation="true"][data-ala="true"][data-type="user-defined"] .type-box.type-custom-location { + display: block; +} + +#ala-custom[data-type="rc"] .dcl-gps-area, +#ala-custom[data-type="gps"] .dcl-rc-region, +#ala-custom[data-type="gps"] .dcl-rc-city { + opacity: 0.3; +} + +#ala-custom[data-type="gps"] .dcl-gps-area, +#ala-custom[data-type="rc"] .dcl-rc-region, +#ala-custom[data-type="rc"] .dcl-rc-city { + opacity: 1; +} + +.panel-link { + padding-left: 30px; +} + +.blur-label { + padding-top: 5px; +} + +.custom-location-description { + min-height: 0; +} + +.custom-location-description p { + padding-bottom: 0; + padding-top: 0; +} + +/** + * Guided tour + */ + +section[data-section="gt"] > div { + top: 0; + height: 100%; +} + +section[role="region"] > header.gt-header:first-child { + background: none; + position: absolute; +} + +section[role="region"] > header.gt-header:first-child a:hover { + background: none; +} + +section[role="region"] > header.gt-header:first-child .icon-close { + background-image: url(images/close.png); +} + +section[role="region"] > div.gt-box { + top: 0; + height: 100%; +} + +.gt-picture { + height: calc(50% - 2rem); + background: #CEE2E3; + overflow: hidden; + text-align: center; +} + +.gt-picture img { + width: 100%; +} + +.gt-description { + overflow: auto; + height: calc(50% - 4rem); +} + +.gt-description h2 { + color: #00caf2; + text-align: center; + font-size: 1.8rem; + line-height: 2rem; + display: block; + margin: 0; + padding: 1.4rem 1rem 1rem 1rem; + font-weight: 300; +} + +.gt-description p { + line-height: 1.5rem; + font-size: 1.1rem; + text-align: center; + color: #585858; + padding: 0 2rem; +} + +.gt-action { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 10; + height: 7rem; + box-sizing: border-box; + margin-right: 1rem; + background-color: #f4f4f4; + pointer-events: auto; + padding-top: 1rem; +} + +.gt-pager { + background: url("images/guided_tour/gt-pager.png") no-repeat center 0; + height: 10px; + left: 1rem; + position: absolute; + top: 0; + width: 100%; +} + +.gt-pager.page-2 { + background-position: 50% -10px; +} + +.gt-pager.page-3 { + background-position: 50% -20px; +} + +.gt-pager.page-4 { + background-position: 50% -30px; +} + +.gt-pager.page-5 { + background-position: 50% -40px; +} + +.gt-pager.page-6 { + background-position: 50% -50px; +} + +.gt-pager.page-7 { + background-position: 50% -60px; +} + +.gt-pager.page-8 { + background-position: 50% -70px; +} + +.gt-pager.page-9 { + background-position: 50% -80px; +} + +.gt-pager.page-10 { + background-position: 50% -90px; +} + +.btn { + position: relative; + font-family: sans-serif; + font-style: italic; + width: 100%; + height: 4rem; + margin: 0; + padding: 0 1.2rem; + box-sizing: border-box; + display: inline-block; + vertical-align: middle; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + background: #d8d8d8; + border: 0.1rem solid #999999; + border-radius: 2rem; + font-weight: normal; + font-size: 1.6rem; + line-height: 4rem; + color: #333; + text-align: center; + text-decoration: none; + outline: none; +} + +.btn.btn-blue { + background-color: #00caf2; + border-color: #00caf2; + color: #fff; +} + +.btn.btn-dark-gray { + background-color: #5f5f5f; + border: none; + color: #fff; +} + +section[data-section="gt"] ul { + height: 100%; +} + +section[data-section="gt"] ul li { + height: 100%; + display: block; + margin: 0; + padding: 0; + border: none; +} + +section[data-section="gt"] ul li > * { + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; +} + +section[data-section="gt"] .btn { + margin: 0 0 0 1rem; +} + + +/** + * Remote Privacy Protection + */ + +.rpp-box { + display: none; +} + +.validation-message, +.pin-validation-message { + color: red; + font-size: 1.4rem; + white-space: normal; + margin: 1rem; +} + + +/** + * Alerts + */ + +.overlay { + position: fixed; + height: 100%; + width: 100%; + top: 0; + left: 0; + background: rgba(0, 0, 0, 0.65); + z-index: 1; + display: flex; + justify-content: center; + align-items: center; +} + +.overlay .modal { + background: #fff; + width: 22rem; + text-align: center; + border-radius: 0.5rem; + border: 0.1rem solid #444; + box-sizing: border-box; + padding: 2rem; +} + +.overlay .modal p, +.overlay .modal .link-text { + font-size: 1.2rem; + padding: 1.5rem 0; +} + +.overlay .modal button { + margin: 0; +} + +.overlay .modal-alert h1 { + margin: 0 0 1rem 0; + padding: 0; + color: #868692; + font-weight: 300; + font-style: italic; + text-align: center; + font-size: 2rem; +} + +.overlay .modal-alert h1 { + color: #e30613; +} + +/** + * RPP auth + */ +#rpp-main[data-login-box="true"] #rpp-login { + display: block; +} + +#rpp-main[data-login-box="false"] #rpp-register { + display: block; +} + + +/** + * Screenlock and passcode panels + */ + +#rpp-screenlock li.lockscreen-enabled { + display: none; +} + +#rpp-screenlock[data-lockscreen-enabled="true"] li.lockscreen-enabled { + display: block; +} + +#rpp-screenlock li.passcode-enabled { + display: none; +} + +#rpp-screenlock[data-lockscreen-enabled="true"][data-passcode-enabled="true"] li.passcode-enabled { + display: block; +} + +#rpp-screenlock li.passcode-enabled { + display: none; +} + +#rpp-screenlock[data-lockscreen-enabled="true"][data-passcode-enabled="true"] li.passcode-enabled { + display: block; +} + +#rpp-passcode .passcode-error { + clear: both; + display: none; + height: 4.2rem; + padding-left: 3rem; + color: red; + font-size: 1.5rem; + line-height: 4.2rem; +} + +#rpp-passcode[data-passcode-status="error"][data-mode="confirm"] div.passcode-error[data-type="incorrect"], +#rpp-passcode[data-passcode-status="error"][data-mode="confirmLock"] div.passcode-error[data-type="incorrect"], +#rpp-passcode[data-passcode-status="error"][data-mode="edit"] div.passcode-error[data-type="incorrect"] { + display: block; +} + +#rpp-passcode[data-passcode-status="error"][data-mode="new"] div.passcode-error[data-type="mismatch"], +#rpp-passcode[data-passcode-status="error"][data-mode="create"] div.passcode-error[data-type="mismatch"] { + display: block; +} + +#rpp-passcode[data-passcode-status="error"] menu[data-mode="new"] { + display: none; +} + +.passcode-change div.passcode-overlay { + position: relative; +} + +#rpp-passcode label { + -moz-box-sizing: border-box; + display: block; + width: auto; + height: auto; + margin: 1.6rem 0 0.4rem; + padding: 0 2rem; + color: #000; + font-size: 1.8rem; +} + +#rpp-passcode [data-mode] { + display: none; +} + +#rpp-passcode menu[type="toolbar"] { + width: 8rem; +} + +#rpp-passcode menu[type="toolbar"] button { + width: 100%; +} + +#rpp-passcode[data-mode="create"] [data-mode*="create"], +#rpp-passcode[data-mode="confirm"] [data-mode*="confirm"], +#rpp-passcode[data-mode="confirmLock"] [data-mode*="confirmLock"], +#rpp-passcode[data-mode="edit"] [data-mode*="edit"], +#rpp-passcode[data-mode="new"] [data-mode*="new"] { + display: block; +} + +#passcode-confirm { + display: none; +} + +#rpp-passcode[data-status="success"] #passcode-confirm { + display: inherit; +} + +.passcode-input { + z-index: -1; + position: absolute; + top: -5rem; + left: 0; +} + +.passcode-container { + position: absolute; + z-index: 1; + width: 100%; + top: 0; + left: 0; + background: url(images/document-bg.png); +} + +.passcode { + width: calc(100% - 2rem); + margin: 0 auto; + overflow: hidden; +} + +.passcode-digit { + -moz-box-sizing: border-box; + position: relative; + float: left; + width: calc(25% - 1rem); + height: 4rem; + margin: 0 0.5rem; + border: 0.1rem solid #c2c2c2; + text-align: center; + background-color: #fff; + border-radius: 0.3rem; +} + +span.passcode-digit[data-dot]::before { + content: ''; + display: block; + position: absolute; + width: 1.5rem; + height: 1.5rem; + background-color: #3e3b39; + border-radius: 0.75rem; + top: 50%; + left: 50%; + margin-left: -0.75rem; + margin-top: -0.75rem; +} + +/** Icons + ---------------------------------------------------------*/ + +[class^="gaia-icon-"]:before, +[class*="gaia-icon-"]:before { + font-family: "gaia-icons"; + position: relative; + top: 0; + left: 0; + font-size: 3rem; + line-height: 5rem; + color: #657073; + font-style: normal; +} + +.gaia-icon-close:before { + content: 'close'; + color: #fff; +} + +.gaia-icon-back:before { + content: 'back'; +} + +.gaia-icon-about:before { + content: 'info'; +} + +/** + * [dir='rtl'] + * + * Switch to use the 'forward' icon + * when in right-to-left direction. + */ + +[dir='rtl'] .icon-back:before { + content: 'forward'; +} diff --git a/apps/privacy-panel/templates/about/main.html b/apps/privacy-panel/templates/about/main.html new file mode 100644 index 000000000000..89185edcfd61 --- /dev/null +++ b/apps/privacy-panel/templates/about/main.html @@ -0,0 +1,34 @@ + + + diff --git a/apps/privacy-panel/templates/ala/custom.html b/apps/privacy-panel/templates/ala/custom.html new file mode 100644 index 000000000000..b43c32d098bf --- /dev/null +++ b/apps/privacy-panel/templates/ala/custom.html @@ -0,0 +1,53 @@ + + + diff --git a/apps/privacy-panel/templates/ala/exception.html b/apps/privacy-panel/templates/ala/exception.html new file mode 100644 index 000000000000..a2b566969471 --- /dev/null +++ b/apps/privacy-panel/templates/ala/exception.html @@ -0,0 +1,61 @@ + + + diff --git a/apps/privacy-panel/templates/ala/exceptions.html b/apps/privacy-panel/templates/ala/exceptions.html new file mode 100644 index 000000000000..69b38ce604a6 --- /dev/null +++ b/apps/privacy-panel/templates/ala/exceptions.html @@ -0,0 +1,19 @@ + + + diff --git a/apps/privacy-panel/templates/ala/main.html b/apps/privacy-panel/templates/ala/main.html new file mode 100644 index 000000000000..3ddc232fd6a8 --- /dev/null +++ b/apps/privacy-panel/templates/ala/main.html @@ -0,0 +1,74 @@ + + + diff --git a/apps/privacy-panel/templates/gt/ala_blur.html b/apps/privacy-panel/templates/gt/ala_blur.html new file mode 100644 index 000000000000..dcccc96b2a7a --- /dev/null +++ b/apps/privacy-panel/templates/gt/ala_blur.html @@ -0,0 +1,29 @@ + + + diff --git a/apps/privacy-panel/templates/gt/ala_custom.html b/apps/privacy-panel/templates/gt/ala_custom.html new file mode 100644 index 000000000000..50cb72e7ee87 --- /dev/null +++ b/apps/privacy-panel/templates/gt/ala_custom.html @@ -0,0 +1,29 @@ + + + diff --git a/apps/privacy-panel/templates/gt/ala_exceptions.html b/apps/privacy-panel/templates/gt/ala_exceptions.html new file mode 100644 index 000000000000..98e103e1b736 --- /dev/null +++ b/apps/privacy-panel/templates/gt/ala_exceptions.html @@ -0,0 +1,29 @@ + + + diff --git a/apps/privacy-panel/templates/gt/ala_explain.html b/apps/privacy-panel/templates/gt/ala_explain.html new file mode 100644 index 000000000000..b0d3fafc42fe --- /dev/null +++ b/apps/privacy-panel/templates/gt/ala_explain.html @@ -0,0 +1,29 @@ + + + diff --git a/apps/privacy-panel/templates/gt/main.html b/apps/privacy-panel/templates/gt/main.html new file mode 100644 index 000000000000..dcef1f7a9f26 --- /dev/null +++ b/apps/privacy-panel/templates/gt/main.html @@ -0,0 +1,28 @@ + + + diff --git a/apps/privacy-panel/templates/gt/rpp_explain.html b/apps/privacy-panel/templates/gt/rpp_explain.html new file mode 100644 index 000000000000..cd110dc7ab3e --- /dev/null +++ b/apps/privacy-panel/templates/gt/rpp_explain.html @@ -0,0 +1,29 @@ + + + diff --git a/apps/privacy-panel/templates/gt/rpp_locate.html b/apps/privacy-panel/templates/gt/rpp_locate.html new file mode 100644 index 000000000000..8c416fabad89 --- /dev/null +++ b/apps/privacy-panel/templates/gt/rpp_locate.html @@ -0,0 +1,31 @@ + + + diff --git a/apps/privacy-panel/templates/gt/rpp_lock.html b/apps/privacy-panel/templates/gt/rpp_lock.html new file mode 100644 index 000000000000..b1f52e64f776 --- /dev/null +++ b/apps/privacy-panel/templates/gt/rpp_lock.html @@ -0,0 +1,31 @@ + + + diff --git a/apps/privacy-panel/templates/gt/rpp_passphrase.html b/apps/privacy-panel/templates/gt/rpp_passphrase.html new file mode 100644 index 000000000000..52bde30828ec --- /dev/null +++ b/apps/privacy-panel/templates/gt/rpp_passphrase.html @@ -0,0 +1,29 @@ + + + diff --git a/apps/privacy-panel/templates/gt/rpp_ring.html b/apps/privacy-panel/templates/gt/rpp_ring.html new file mode 100644 index 000000000000..082a37fee0b4 --- /dev/null +++ b/apps/privacy-panel/templates/gt/rpp_ring.html @@ -0,0 +1,31 @@ + + + diff --git a/apps/privacy-panel/templates/rpp/change_pass.html b/apps/privacy-panel/templates/rpp/change_pass.html new file mode 100644 index 000000000000..a4c4f3e4b482 --- /dev/null +++ b/apps/privacy-panel/templates/rpp/change_pass.html @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/apps/privacy-panel/templates/rpp/features.html b/apps/privacy-panel/templates/rpp/features.html new file mode 100644 index 000000000000..bee84c2e842a --- /dev/null +++ b/apps/privacy-panel/templates/rpp/features.html @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/apps/privacy-panel/templates/rpp/main.html b/apps/privacy-panel/templates/rpp/main.html new file mode 100644 index 000000000000..2b6ce992173a --- /dev/null +++ b/apps/privacy-panel/templates/rpp/main.html @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/apps/privacy-panel/templates/rpp/passcode.html b/apps/privacy-panel/templates/rpp/passcode.html new file mode 100644 index 000000000000..3d31a3ddbc57 --- /dev/null +++ b/apps/privacy-panel/templates/rpp/passcode.html @@ -0,0 +1,39 @@ + + + diff --git a/apps/privacy-panel/templates/rpp/screenlock.html b/apps/privacy-panel/templates/rpp/screenlock.html new file mode 100644 index 000000000000..ebb90751f00d --- /dev/null +++ b/apps/privacy-panel/templates/rpp/screenlock.html @@ -0,0 +1,44 @@ + + + diff --git a/apps/privacy-panel/test/marionette/.jshintrc b/apps/privacy-panel/test/marionette/.jshintrc new file mode 100644 index 000000000000..6f28e6fc410a --- /dev/null +++ b/apps/privacy-panel/test/marionette/.jshintrc @@ -0,0 +1,19 @@ +{ + "extends": "../../.jshintrc", + "predef": [ + "module", + "suite", + "test", + "setup", + "teardown", + "suiteSetup", + "suiteTeardown", + "define", + "require", + "requirejs", + "requireApp", + "sinon", + "mocha", + "marionette" + ] +} diff --git a/apps/privacy-panel/test/marionette/ala_main_test.js b/apps/privacy-panel/test/marionette/ala_main_test.js new file mode 100644 index 000000000000..ab57c4d072ca --- /dev/null +++ b/apps/privacy-panel/test/marionette/ala_main_test.js @@ -0,0 +1,95 @@ +'use strict'; + +var assert = require('assert'); +var AlaMainPanel = require('./lib/panels/ala_main'); + +marionette('check ala main panel', function() { + var client = marionette.client({ + settings: { + 'privacy-panel-gt-complete': true, + 'geolocation.enabled': false, + 'ala.settings.enabled': false, + 'geolocation.type': 'no-location' + } + }); + var subject; + + setup(function() { + subject = new AlaMainPanel(client); + subject.init(); + }); + + test('ability to set geolocation and use location adjustment', function() { + var useLocationBlurBox = client.findElement('.show-when-geolocation-on'); + var geolocationTypeBox = client.findElement('.geolocation-type-box'); + var description1 = client.findElement('.hide-when-ala-on'); + var description2 = client.findElement('.show-when-ala-on .description'); + var addExceptionBox = client.findElement('.add-exception-box'); + var typeBlur = client.findElement('.type-blur'); + var typeCustom = client.findElement('.type-custom-location'); + var geolocationSwitcher = client.findElement( + 'span[data-l10n-id="use-geolocation"]'); + var alaSwitcher = client.findElement( + 'span[data-l10n-id="use-location-adjustment"]'); + + assert.ok(!useLocationBlurBox.displayed()); + + + // turn geolocation on + geolocationSwitcher.tap(); + client.waitFor(function() { + return useLocationBlurBox.displayed(); + }); + assert.ok(description1.displayed()); + assert.ok(!description2.displayed()); + + + // turn use location adjustment on + alaSwitcher.click(); + client.waitFor(function() { + return geolocationTypeBox.displayed(); + }); + assert.ok(!description1.displayed()); + assert.ok(description2.displayed()); + assert.ok(!typeBlur.displayed()); + assert.ok(!typeCustom.displayed()); + + + /**@todo: test select values change */ + + + // turn geolocation off + geolocationSwitcher.click(); + client.waitFor(function() { + return !useLocationBlurBox.displayed(); + }); + assert.ok(!geolocationTypeBox.displayed()); + + + // turn geolocation on + geolocationSwitcher.click(); + client.waitFor(function() { + return useLocationBlurBox.displayed(); + }); + assert.ok(geolocationTypeBox.displayed()); + + + // turn use location adjustment off + alaSwitcher.click(); + client.waitFor(function() { + return !geolocationTypeBox.displayed(); + }); + assert.ok(description1.displayed()); + assert.ok(!description2.displayed()); + assert.ok(!typeBlur.displayed()); + assert.ok(!typeCustom.displayed()); + assert.ok(!addExceptionBox.displayed()); + + + // turn geolocation off + geolocationSwitcher.click(); + client.waitFor(function() { + return !useLocationBlurBox.displayed(); + }); + }); +}); \ No newline at end of file diff --git a/apps/privacy-panel/test/marionette/ftu_guided_tour_test.js b/apps/privacy-panel/test/marionette/ftu_guided_tour_test.js new file mode 100644 index 000000000000..95984df9e62f --- /dev/null +++ b/apps/privacy-panel/test/marionette/ftu_guided_tour_test.js @@ -0,0 +1,20 @@ +'use strict'; + +var assert = require('assert'); +var GtPanels = require('./lib/panels/ftu_guided_tour'); + +marionette('first time use', function() { + var client = marionette.client({}); + var subject; + + setup(function() { + subject = new GtPanels(client); + subject.init(); + }); + + test('ability to get guided tour panel at ftu', function() { + assert.ok(subject.isGtWelcomeDisplayed()); + subject.tapOnCloseBtn(); + assert.ok(subject.isRootDisplayed()); + }); +}); diff --git a/apps/privacy-panel/test/marionette/guided_tour_test.js b/apps/privacy-panel/test/marionette/guided_tour_test.js new file mode 100644 index 000000000000..274b3ea91798 --- /dev/null +++ b/apps/privacy-panel/test/marionette/guided_tour_test.js @@ -0,0 +1,61 @@ +'use strict'; + +var GtPanels = require('./lib/panels/guided_tour'); + +marionette('guided tour panels', function() { + var client = marionette.client({ + settings: { + 'privacy-panel-gt-complete': true + } + }); + var subject; + + setup(function() { + subject = new GtPanels(client); + subject.init(); + }); + + test('ability to get through guided tour flow', function() { + subject.getThruPanel(subject.selectors.gtWelcome); + subject.getThruPanel(subject.selectors.gtAlaExplain); + subject.getThruPanel(subject.selectors.gtAlaBlur); + subject.getThruPanel(subject.selectors.gtAlaCustom); + subject.getThruPanel(subject.selectors.gtAlaExceptions); + subject.getThruPanel(subject.selectors.gtRppExplain); + subject.getThruPanel(subject.selectors.gtRppPassphrase); + subject.getThruPanel(subject.selectors.gtRppLocate); + subject.getThruPanel(subject.selectors.gtRppRing); + subject.getThruPanel(subject.selectors.gtRppLock); + }); + + test('ability to close guided tour after 4ths screen', function() { + subject.getThruPanel(subject.selectors.gtWelcome); + subject.getThruPanel(subject.selectors.gtAlaExplain); + subject.getThruPanel(subject.selectors.gtAlaBlur); + subject.getThruPanel(subject.selectors.gtAlaCustom); + subject.tapOnCloseBtn(subject.selectors.gtAlaExceptions); + subject.waitForPanel(subject.selectors.rootPanel); + }); + + test('ability to get back to previous gt panels', function() { + subject.getThruPanel(subject.selectors.gtWelcome); + subject.getThruPanel(subject.selectors.gtAlaExplain); + subject.getThruPanel(subject.selectors.gtAlaBlur); + subject.getThruPanel(subject.selectors.gtAlaCustom); + subject.getThruPanel(subject.selectors.gtAlaExceptions); + subject.getThruPanel(subject.selectors.gtRppExplain); + subject.getThruPanel(subject.selectors.gtRppPassphrase); + subject.getThruPanel(subject.selectors.gtRppLocate); + subject.getThruPanel(subject.selectors.gtRppRing); + subject.getBack(subject.selectors.gtRppLock); + subject.getBack(subject.selectors.gtRppRing); + subject.getBack(subject.selectors.gtRppLocate); + subject.getBack(subject.selectors.gtRppPassphrase); + subject.getBack(subject.selectors.gtRppExplain); + subject.getBack(subject.selectors.gtAlaExceptions); + subject.getBack(subject.selectors.gtAlaCustom); + subject.getBack(subject.selectors.gtAlaBlur); + subject.getBack(subject.selectors.gtAlaExplain); + subject.waitForPanel(subject.selectors.gtWelcome); + }); +}); diff --git a/apps/privacy-panel/test/marionette/lib/base.js b/apps/privacy-panel/test/marionette/lib/base.js new file mode 100644 index 000000000000..f15c2d1abec2 --- /dev/null +++ b/apps/privacy-panel/test/marionette/lib/base.js @@ -0,0 +1,87 @@ +'use strict'; +/** + * Base app object to provide common methods to app objects + * + * @constructor + * @param {Marionette.Client} client for operations. + */ +function Base(client, origin) { + this.client = client.scope({ searchTimeout: 20000 }); + this.origin = origin || 'app://privacy-panel.gaiamobile.org'; +} + +module.exports = Base; + +Base.prototype = { + + /** + * Launches privacy-panel app. + */ + launch: function() { + this.client.apps.launch(this.origin); + this.client.apps.switchToApp(this.origin); + this.client.helper.waitForElement('body[data-ready="true"]'); + }, + + /** + * Switches back to the current app frame. + * Useful when switching to system frame during test and needs to switch back. + */ + switchTo: function(origin) { + this.client.switchToFrame(); + this.client.apps.switchToApp(origin); + }, + + /** + * @protected + * @param {String} name of selector + */ + waitForElement: function(name) { + return this.client.helper.waitForElement(name); + }, + + /** + * Waits for panel to dissapear, due to transition and transform we need to + * be sure element is not in the viewport range. + * + * @protected + * @param {String} name of selector + */ + waitForPanelToDissapear: function(name) { + var element = this.waitForElement(name); + this.client.waitFor(function() { + var loc = element.location(); + var size = element.size(); + var position = loc.x + size.width; + + // Depending on transition behaviour element can be hidden + // near left corner (position = 0) or right corner + // (position = 640 on simulator - two times scren size) + return position === 0 || position === (2 * size.width); + }); + }, + + /** + * @protected + * @param {String} name of selector + */ + waitForPanel: function(name) { + var element = this.waitForElement(name); + this.client.waitFor(function() { + var loc = element.location(); + return loc.x === 0; + }); + }, + + /** + * Use to select an options as currently we are not able to tap on a select + * element. Please refer to bug 977522 for details. + * + * @protected + * @param {String} name of selector. + * @param {String} text content of an option + */ + tapSelectOption: function(name, optionText) { + this.client.helper.tapSelectOption(name, optionText); + } +}; diff --git a/apps/privacy-panel/test/marionette/lib/panels/ala_main.js b/apps/privacy-panel/test/marionette/lib/panels/ala_main.js new file mode 100644 index 000000000000..26e83c503493 --- /dev/null +++ b/apps/privacy-panel/test/marionette/lib/panels/ala_main.js @@ -0,0 +1,26 @@ +'use strict'; + +var Base = require('../base'); + +function AlaMainPanel(client) { + Base.call(this, client); +} + +module.exports = AlaMainPanel; + +AlaMainPanel.prototype = { + + __proto__: Base.prototype, + + selectors: { + rootPanel: '#root', + alaPanel: '#ala-main' + }, + + init: function() { + this.launch(); + this.client.findElement('#menu-item-ala').tap(); + this.waitForPanelToDissapear(this.selectors.rootPanel); + } + +}; diff --git a/apps/privacy-panel/test/marionette/lib/panels/ftu_guided_tour.js b/apps/privacy-panel/test/marionette/lib/panels/ftu_guided_tour.js new file mode 100644 index 000000000000..e4df580996f6 --- /dev/null +++ b/apps/privacy-panel/test/marionette/lib/panels/ftu_guided_tour.js @@ -0,0 +1,38 @@ +'use strict'; + +var Base = require('../base'); + +function GuidedTourPanels(client) { + Base.call(this, client); +} + +module.exports = GuidedTourPanels; + +GuidedTourPanels.prototype = { + + __proto__: Base.prototype, + + selectors: { + rootPanel: '#root', + gtWelcome: '#gt-main' + }, + + init: function() { + this.launch(); + }, + + tapOnCloseBtn: function() { + this.client.findElement(this.selectors.gtWelcome + ' .gt-header .back') + .tap(); + }, + + isRootDisplayed: function() { + this.waitForPanel(this.selectors.rootPanel); + return this.client.findElement(this.selectors.rootPanel).displayed(); + }, + + isGtWelcomeDisplayed: function() { + return this.client.findElement(this.selectors.gtWelcome).displayed(); + } + +}; diff --git a/apps/privacy-panel/test/marionette/lib/panels/guided_tour.js b/apps/privacy-panel/test/marionette/lib/panels/guided_tour.js new file mode 100644 index 000000000000..f8d2258e9bd6 --- /dev/null +++ b/apps/privacy-panel/test/marionette/lib/panels/guided_tour.js @@ -0,0 +1,62 @@ +'use strict'; + +var Base = require('../base'); + +function GuidedTourPanels(client) { + Base.call(this, client); +} + +module.exports = GuidedTourPanels; + +GuidedTourPanels.prototype = { + + __proto__: Base.prototype, + + selectors: { + rootPanel: '#root', + gtWelcome: '#gt-main', + gtAlaExplain: '#gt-ala-explain', + gtAlaBlur: '#gt-ala-blur', + gtAlaCustom: '#gt-ala-custom', + gtAlaExceptions: '#gt-ala-exceptions', + gtRppExplain: '#gt-rpp-explain', + gtRppPassphrase: '#gt-rpp-passphrase', + gtRppLocate: '#gt-rpp-locate', + gtRppRing: '#gt-rpp-ring', + gtRppLock: '#gt-rpp-lock' + }, + + init: function() { + this.launch(); + this.client.findElement('#menu-item-gt').tap(); + this.waitForPanelToDissapear(this.selectors.rootPanel); + }, + + tapOnNextBtn: function(panel) { + this.client.findElement(panel + ' .btn-blue').tap(); + }, + + tapOnBackBtn: function(panel) { + this.client.findElement(panel + ' .btn-dark-gray').tap(); + }, + + tapOnCloseBtn: function(panel) { + this.client.findElement(panel + ' .gt-header .back').tap(); + }, + + isRootDisplayed: function() { + this.waitForPanel(this.selectors.rootPanel); + return this.client.findElement(this.selectors.rootPanel).displayed(); + }, + + getThruPanel: function(panel) { + this.tapOnNextBtn(panel); + this.waitForPanelToDissapear(panel); + }, + + getBack: function(panel) { + this.tapOnBackBtn(panel); + this.waitForPanelToDissapear(panel); + } + +}; diff --git a/apps/privacy-panel/test/marionette/lib/panels/root.js b/apps/privacy-panel/test/marionette/lib/panels/root.js new file mode 100644 index 000000000000..6f4234759b94 --- /dev/null +++ b/apps/privacy-panel/test/marionette/lib/panels/root.js @@ -0,0 +1,53 @@ +'use strict'; + +var Base = require('../base'); + +function RootPanel(client) { + Base.call(this, client); +} + +module.exports = RootPanel; + +RootPanel.prototype = { + + __proto__: Base.prototype, + + selectors: { + rootPanel: '#root', + alaPanel: '#ala-main', + rppPanel: '#rpp-main', + gtPanel: '#gt-main' + }, + + init: function() { + this.launch(); + }, + + tapOnAlaMenuItem: function() { + this.client.findElement('#menu-item-ala').tap(); + }, + + tapOnRppMenuItem: function() { + this.client.findElement('#menu-item-rpp').tap(); + }, + + tapOnGtMenuItem: function() { + this.client.findElement('#menu-item-gt').tap(); + }, + + isAlaDisplayed: function() { + this.waitForPanelToDissapear(this.selectors.rootPanel); + return this.client.findElement(this.selectors.alaPanel).displayed(); + }, + + isRppDisplayed: function() { + this.waitForPanelToDissapear(this.selectors.rootPanel); + return this.client.findElement(this.selectors.rppPanel).displayed(); + }, + + isGtDisplayed: function() { + this.waitForPanelToDissapear(this.selectors.rootPanel); + return this.client.findElement(this.selectors.gtPanel).displayed(); + }, + +}; diff --git a/apps/privacy-panel/test/marionette/lib/panels/rpp_features.js b/apps/privacy-panel/test/marionette/lib/panels/rpp_features.js new file mode 100644 index 000000000000..7ebd4e9cf33d --- /dev/null +++ b/apps/privacy-panel/test/marionette/lib/panels/rpp_features.js @@ -0,0 +1,106 @@ +'use strict'; + +var Base = require('../base'); + +function AlaMainPanel(client) { + Base.call(this, client); +} + +module.exports = AlaMainPanel; + +AlaMainPanel.prototype = { + + __proto__: Base.prototype, + + selectors: { + rootPanel: '#root', + rppPanel: '#rpp-main', + featuresPanel: '#rpp-features', + registerForm: '#rpp-register', + registerPass1: '#rpp-register .pass1', + registerPass2: '#rpp-register .pass2', + registerSubmit: '#rpp-register .rpp-register-ok', + lockInput: '#rpp-features [name="rpp.lock.enabled"]', + lockLabel: '#rpp-features [data-l10n-id="remote-lock"]', + ringInput: '#rpp-features [name="rpp.ring.enabled"]', + ringLabel: '#rpp-features [data-l10n-id="remote-ring"]', + locateInput: '#rpp-features [name="rpp.locate.enabled"]', + locateLabel: '#rpp-features [data-l10n-id="remote-locate"]', + alert: '#rpp-features .overlay' + }, + + init: function() { + this.launch(); + this.loadMainPanel(); + this.registerUser(); + }, + + loadMainPanel: function() { + this.client.findElement('#menu-item-rpp').tap(); + this.waitForPanelToDissapear(this.selectors.rootPanel); + }, + + registerUser: function() { + this.typeNewPassphrase('mypassword'); + this.waitForPanelToDissapear(this.selectors.rppPanel); + }, + + typeNewPassphrase: function(passphrase) { + this.client.findElement(this.selectors.registerPass1).sendKeys(passphrase); + this.client.findElement(this.selectors.registerPass2).sendKeys(passphrase); + this.client.findElement(this.selectors.registerSubmit).tap(); + }, + + isFeaturesPanelDisplayed: function() { + return this.client.findElement(this.selectors.featuresPanel).displayed(); + }, + + isAlertDisplayed: function() { + return this.client.findElement(this.selectors.alert).displayed(); + }, + + isLockChecked: function() { + return this.client.findElement(this.selectors.lockInput) + .getAttribute('checked'); + }, + + isRingChecked: function() { + return this.client.findElement(this.selectors.ringInput) + .getAttribute('checked'); + }, + + isLocateChecked: function() { + return this.client.findElement(this.selectors.locateInput) + .getAttribute('checked'); + }, + + isLockEnabled: function() { + return this.client.settings.get('rpp.lock.enabled'); + }, + + isRingEnabled: function() { + return this.client.settings.get('rpp.ring.enabled'); + }, + + isLocateEnabled: function() { + return this.client.settings.get('rpp.locate.enabled'); + }, + + tapBackBtn: function(panel) { + this.client.findElement(panel + ' header .back').tap(); + this.waitForPanelToDissapear(panel); + }, + + tapOnLock: function() { + this.client.findElement(this.selectors.lockLabel).tap(); + }, + + tapOnRing: function() { + this.client.findElement(this.selectors.ringLabel).tap(); + }, + + tapOnLocate: function() { + this.client.findElement(this.selectors.locateLabel).tap(); + } + +}; diff --git a/apps/privacy-panel/test/marionette/lib/panels/rpp_main.js b/apps/privacy-panel/test/marionette/lib/panels/rpp_main.js new file mode 100644 index 000000000000..3c466a201bd8 --- /dev/null +++ b/apps/privacy-panel/test/marionette/lib/panels/rpp_main.js @@ -0,0 +1,66 @@ +'use strict'; + +var Base = require('../base'); + +function AlaMainPanel(client) { + Base.call(this, client); +} + +module.exports = AlaMainPanel; + +AlaMainPanel.prototype = { + + __proto__: Base.prototype, + + selectors: { + rootPanel: '#root', + rppPanel: '#rpp-main', + featuresPanel: '#rpp-features', + loginForm: '#rpp-login', + loginPass: '#rpp-login .pass1', + loginSubmit: '#rpp-login .rpp-login-ok', + registerForm: '#rpp-register', + registerPass1: '#rpp-register .pass1', + registerPass2: '#rpp-register .pass2', + registerSubmit: '#rpp-register .rpp-register-ok' + }, + + init: function() { + this.launch(); + this.loadMainPanel(); + }, + + loadMainPanel: function() { + this.client.findElement('#menu-item-rpp').tap(); + this.waitForPanelToDissapear(this.selectors.rootPanel); + }, + + isRegisterFormDisplayed: function() { + return this.client.findElement(this.selectors.registerForm).displayed(); + }, + + isLoginFormDisplayed: function() { + return this.client.findElement(this.selectors.loginForm).displayed(); + }, + + typePassphrase: function(passphrase) { + this.client.findElement(this.selectors.loginPass).sendKeys(passphrase); + this.client.findElement(this.selectors.loginSubmit).tap(); + }, + + typeNewPassphrase: function(passphrase) { + this.client.findElement(this.selectors.registerPass1).sendKeys(passphrase); + this.client.findElement(this.selectors.registerPass2).sendKeys(passphrase); + this.client.findElement(this.selectors.registerSubmit).tap(); + }, + + isFeaturesPanelDisplayed: function() { + return this.client.findElement(this.selectors.featuresPanel).displayed(); + }, + + tapBackBtn: function(panel) { + this.client.findElement(panel + ' header .back').tap(); + this.waitForPanelToDissapear(panel); + } + +}; diff --git a/apps/privacy-panel/test/marionette/lib/panels/settings_app.js b/apps/privacy-panel/test/marionette/lib/panels/settings_app.js new file mode 100644 index 000000000000..126ac7b42164 --- /dev/null +++ b/apps/privacy-panel/test/marionette/lib/panels/settings_app.js @@ -0,0 +1,32 @@ +'use strict'; + +var Base = require('../base'); + +function PrivacyPanelApp(client) { + Base.call(this, client, 'app://settings.gaiamobile.org'); +} + +module.exports = PrivacyPanelApp; + +PrivacyPanelApp.prototype = { + + __proto__: Base.prototype, + + selectors: { + ppMenuItem: '.privacy-panel-item', + backToSettings: '#back-to-settings' + }, + + init: function() { + this.launch(); + }, + + tapOnMenuItem: function() { + this.client.findElement(this.selectors.ppMenuItem).tap(); + }, + + tapOnBackToSettingsBtn: function() { + this.client.findElement(this.selectors.backToSettings).tap(); + } + +}; diff --git a/apps/privacy-panel/test/marionette/manifest.ini b/apps/privacy-panel/test/marionette/manifest.ini new file mode 100644 index 000000000000..0afb196ad0c9 --- /dev/null +++ b/apps/privacy-panel/test/marionette/manifest.ini @@ -0,0 +1,3 @@ +[root_test.js] +[guided_tour_test.js] +[rpp_main_test.js] diff --git a/apps/privacy-panel/test/marionette/root_test.js b/apps/privacy-panel/test/marionette/root_test.js new file mode 100644 index 000000000000..7153d0a63d1b --- /dev/null +++ b/apps/privacy-panel/test/marionette/root_test.js @@ -0,0 +1,38 @@ +'use strict'; + +var assert = require('assert'); +var RootPanel = require('./lib/panels/root'); + +marionette('root panel', function() { + var client = marionette.client({ + settings: { + 'privacy-panel-gt-complete': true + } + }); + var subject; + + setup(function() { + subject = new RootPanel(client); + subject.init(); + }); + + test('root page elements', function() { + var menuItems = client.findElements('#root li'); + assert.ok(menuItems.length === 3); + }); + + test('ability to load ala panel', function() { + subject.tapOnAlaMenuItem(); + assert.ok(subject.isAlaDisplayed()); + }); + + test('ability to load rpp panel', function() { + subject.tapOnRppMenuItem(); + assert.ok(subject.isRppDisplayed()); + }); + + test('ability to load guided tour panel', function() { + subject.tapOnGtMenuItem(); + assert.ok(subject.isGtDisplayed()); + }); +}); diff --git a/apps/privacy-panel/test/marionette/rpp_features_test.js b/apps/privacy-panel/test/marionette/rpp_features_test.js new file mode 100644 index 000000000000..60e8e103ba95 --- /dev/null +++ b/apps/privacy-panel/test/marionette/rpp_features_test.js @@ -0,0 +1,57 @@ +'use strict'; + +var assert = require('assert'); +var RppMainPanel = require('./lib/panels/rpp_features'); + +marionette('remote privacy protection main panel', function() { + var client = marionette.client({ + settings: { + 'privacy-panel-gt-complete': true, + 'lockscreen.enabled': true, + 'lockscreen.passcode-lock.enabled': true, + 'rpp.locate.enabled': false, + 'rpp.ring.enabled': false, + 'rpp.lock.enabled': false + } + }); + var subject; + + setup(function() { + subject = new RppMainPanel(client); + subject.init(); + }); + + test('ability to show features panel', function() { + assert.ok(subject.isFeaturesPanelDisplayed()); + assert.ok(!subject.isAlertDisplayed()); + }); + + test('ability to toggle "locate/ring/lock" features', function() { + // Enable all + subject.tapOnLocate(); + assert.ok(subject.isLocateChecked()); + assert.ok(subject.isLocateEnabled()); + + subject.tapOnRing(); + assert.ok(subject.isRingChecked()); + assert.ok(subject.isRingEnabled()); + + subject.tapOnLock(); + assert.ok(subject.isLockChecked()); + assert.ok(subject.isLockEnabled()); + + // Disable all + subject.tapOnLocate(); + assert.ok(!subject.isLocateChecked()); + assert.ok(!subject.isLocateEnabled()); + + subject.tapOnRing(); + assert.ok(!subject.isRingChecked()); + assert.ok(!subject.isRingEnabled()); + + subject.tapOnLock(); + assert.ok(!subject.isLockChecked()); + assert.ok(!subject.isLockEnabled()); + }); + +}); diff --git a/apps/privacy-panel/test/marionette/rpp_main_test.js b/apps/privacy-panel/test/marionette/rpp_main_test.js new file mode 100644 index 000000000000..945c581c9d1f --- /dev/null +++ b/apps/privacy-panel/test/marionette/rpp_main_test.js @@ -0,0 +1,46 @@ +'use strict'; + +var assert = require('assert'); +var RppMainPanel = require('./lib/panels/rpp_main'); + +marionette('remote privacy protection main panel', function() { + var client = marionette.client({ + settings: { + 'privacy-panel-gt-complete': true + } + }); + var subject; + + setup(function() { + subject = new RppMainPanel(client); + subject.init(); + }); + + test('ftu register form is displayed', function() { + assert.ok(subject.isRegisterFormDisplayed()); + assert.ok(!subject.isLoginFormDisplayed()); + }); + + test('ability to register with given passphrase', function() { + subject.typeNewPassphrase('mypassword'); + subject.waitForPanelToDissapear(subject.selectors.rppPanel); + + assert.ok(subject.isFeaturesPanelDisplayed()); + }); + + test('after register we can login using our passphrase', function() { + subject.typeNewPassphrase('mypassword'); + subject.waitForPanelToDissapear(subject.selectors.rppPanel); + subject.tapBackBtn(subject.selectors.featuresPanel); + subject.loadMainPanel(); + + assert.ok(subject.isLoginFormDisplayed()); + assert.ok(!subject.isRegisterFormDisplayed()); + + subject.typePassphrase('mypassword'); + subject.waitForPanelToDissapear(subject.selectors.rppPanel); + + assert.ok(subject.isFeaturesPanelDisplayed()); + }); + +}); diff --git a/apps/privacy-panel/test/marionette/running_app_test.js b/apps/privacy-panel/test/marionette/running_app_test.js new file mode 100644 index 000000000000..627ecff4449b --- /dev/null +++ b/apps/privacy-panel/test/marionette/running_app_test.js @@ -0,0 +1,38 @@ +'use strict'; + +var SettingsApp = require('./lib/panels/settings_app'); + +marionette('settings app', function() { + var client = marionette.client({ + settings: { + 'privacy-panel-gt-complete': true + } + }); + var subject; + + setup(function() { + subject = new SettingsApp(client); + subject.init(); + }); + + test('loading privacy-panel app from settings', function() { + subject.tapOnMenuItem(); + + // Change marionette context to privacy-panel app. + subject.switchTo('app://privacy-panel.gaiamobile.org'); + + client.waitFor(function() { + return client.findElement(subject.selectors.backToSettings).displayed(); + }); + + subject.tapOnBackToSettingsBtn(); + + // Change marionette context back to settings app. + subject.switchTo('app://settings.gaiamobile.org'); + + client.waitFor(function() { + return client.findElement(subject.selectors.ppMenuItem).displayed(); + }); + }); + +}); diff --git a/apps/privacy-panel/test/unit/.jshintrc b/apps/privacy-panel/test/unit/.jshintrc new file mode 100644 index 000000000000..9ce644ca4c71 --- /dev/null +++ b/apps/privacy-panel/test/unit/.jshintrc @@ -0,0 +1,18 @@ +{ + "extends": "../../.jshintrc", + "predef": [ + "assert", + "suite", + "test", + "setup", + "teardown", + "suiteSetup", + "suiteTeardown", + "define", + "require", + "requirejs", + "requireApp", + "sinon", + "mocha" + ] +} diff --git a/apps/privacy-panel/test/unit/ala/app_list_test.js b/apps/privacy-panel/test/unit/ala/app_list_test.js new file mode 100644 index 000000000000..9c6c96269c03 --- /dev/null +++ b/apps/privacy-panel/test/unit/ala/app_list_test.js @@ -0,0 +1,110 @@ +'use strict'; + +var realMozApps; +var fakeApp1; +var fakeApp2; + +suite('ALA AppList', function() { + suiteSetup(function(done) { + + var apps = [{ + manifest: { + name: 'Mozilla Fake App 1', + launch_path: '/fakeapp1/index.html', + permissions: { + permission_1: {} + }, + icons: { + 84: '/style/icons/settings_84.png', + 126: '/style/icons/settings_126.png', + 142: 'style/icons/settings_142.png', + 189: '/style/icons/settings_189.png', + 284: '/style/icons/settings_284.png' + } + }, + manifestURL: 'http://fakeapp1/manifest.webapp' + }, { + manifest: { + name: 'Mozilla Fake App 2', + launch_path: '/fakeapp2/index.html', + permissions: { + permission_2: {} + } + }, + manifestURL: 'http://fakeapp2/manifest.webapp' + }, { + manifest: { + name: 'Mozilla Fake App 3', + launch_path: '/fakeapp3/index.html', + permissions: { + permission_2: {} + } + } + }]; + + require(['mocks/mock_navigator_moz_apps'], function(mozApps) { + realMozApps = navigator.mozApps; + navigator.mozApps = mozApps; + navigator.mozApps.mApps = apps; + done(); + }); + }); + + setup(function(done) { + require(['ala/app_list'], appList => { + this.subject = appList; + done(); + }); + }); + + suiteTeardown(function() { + navigator.mozApps = realMozApps; + }); + + test('should get one apps', function(done) { + this.subject.get('permission_1', function(result) { + assert.lengthOf(result, 1); + assert.equal(result[0].manifest.name, 'Mozilla Fake App 1'); + fakeApp1 = result[0]; + done(); + }); + }); + + test('should get list of apps', function(done) { + this.subject.get('permission_2', function(result) { + assert.lengthOf(result, 2); + + assert.equal(result[0].manifest.name, 'Mozilla Fake App 2'); + fakeApp2 = result[0]; + + assert.equal(result[1].manifest.name, 'Mozilla Fake App 3'); + done(); + }); + }); + + test('should get empty list of apps when we are giving invalid permission', + function(done) { + this.subject.get('invalidPermission', function(result) { + assert.isArray(result); + assert.lengthOf(result, 0); + done(); + }); + } + ); + + test('should get path to app icon', + function(done) { + var icon1 = this.subject.icon(fakeApp1); + assert.notEqual(icon1, '../style/images/default.png'); + done(); + } + ); + + test('should get default path to app icon if path is not set in manifest', + function(done) { + var icon2 = this.subject.icon(fakeApp2); + assert.equal(icon2, '../style/images/default.png'); + done(); + } + ); +}); diff --git a/apps/privacy-panel/test/unit/ala/blur_slider_test.js b/apps/privacy-panel/test/unit/ala/blur_slider_test.js new file mode 100644 index 000000000000..b5311cdbe5e4 --- /dev/null +++ b/apps/privacy-panel/test/unit/ala/blur_slider_test.js @@ -0,0 +1,49 @@ +'use strict'; + +suite('ALA BlurSlider', function() { + + setup(function(done) { + require(['ala/blur_slider'], BlurSlider => { + var slider = document.createElement('input'); + var label = document.createElement('p'); + slider.classList.add('blur-slider'); + label.classList.add('blur-label'); + this.element = document.createElement('div'); + this.element.appendChild(label); + this.element.appendChild(slider); + this.subject = new BlurSlider(); + done(); + }); + }); + + //initialize with 500m + test('initializing the element', function() { + this.subject.init(this.element, '1'); + var label = this.element.querySelector('.blur-label').innerHTML; + assert.equal(label, '500m'); + }); + + test('check changing to 1 km ', function(done) { + this.subject.init(this.element, '1', function(result) { + assert.equal(result, 1); + assert.isNumber(result); + done(); + }); + + this.element.querySelector('.blur-slider').value = 2; + + var event = new Event('change'); + this.subject.input.dispatchEvent(event); + }); + + test('check changing the slider', function() { + this.subject.init(this.element,'1'); + this.element.querySelector('.blur-slider').value = 2; + + var event = new Event('touchmove'); + this.subject.input.dispatchEvent(event); + + var label = this.element.querySelector('.blur-label').innerHTML; + assert.equal(label, '1km'); + }); +}); diff --git a/apps/privacy-panel/test/unit/ala/define_custom_location_test.js b/apps/privacy-panel/test/unit/ala/define_custom_location_test.js new file mode 100644 index 000000000000..76599f5157bc --- /dev/null +++ b/apps/privacy-panel/test/unit/ala/define_custom_location_test.js @@ -0,0 +1,161 @@ +'use strict'; + +var htmlHelper; +//var realMozSettings; + +suite('ALA CustomLocation', function() { + + suiteSetup(function(done) { + require(['html_helper'], function(html) { + htmlHelper = html; + done(); + }); + }); + + setup(function(done) { + require(['ala/define_custom_location'], ALADefineCustomLocation => { + var section, test; + + this.subject = ALADefineCustomLocation; + this.template = htmlHelper.get + ('../../templates/ala/custom.html'); + + test = document.getElementById('test'); + section = document.createElement('section'); + section.id = 'ala-custom'; + section.innerHTML = this.template; + + test.appendChild(section); + + done(); + }); + }); + + test('should validate true coords: 0, 0', function() { + this.subject.config = { + latitude: '0', + longitude: '0' + }; + assert.isTrue(this.subject.validate()); + }); + + test('should validate true coords: 0.1, 0.000001', function() { + this.subject.config = { + latitude: '0.1', + longitude: '0.000001' + }; + assert.isTrue(this.subject.validate()); + }); + + test('should validate true coords: -90.000000, -180.000000', function() { + this.subject.config = { + latitude: '-90.000000', + longitude: '-180.000000' + }; + assert.isTrue(this.subject.validate()); + }); + + test('should validate true coords: 52.229675, 21.012228', function() { + this.subject.config = { + latitude: '52.229675', + longitude: '21.012228' + }; + assert.isTrue(this.subject.validate()); + }); + + test('should validate false coords: 90.4, 0', function() { + this.subject.config = { + latitude: '90.4', + longitude: '0' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false coords: 0, -180.51', function() { + this.subject.config = { + latitude: '0', + longitude: '-180.51' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false coords: 15., 0', function() { + this.subject.config = { + latitude: '15.', + longitude: '0' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false coords: 0, 15.', function() { + this.subject.config = { + latitude: '0', + longitude: '15.' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false coords: 5.2.3, 0', function() { + this.subject.config = { + latitude: '5.2.3', + longitude: '0' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false coords: 0, 10.1.2', function() { + this.subject.config = { + latitude: '0', + longitude: '10.1.2' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false coords: 90.0000000, 0', function() { + this.subject.config = { + latitude: '90.0000000', + longitude: '0' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false coords: 0, -18.5100125', function() { + this.subject.config = { + latitude: '0', + longitude: '-18.5100125' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false coords: 52.22d55, 0', function() { + this.subject.config = { + latitude: '52.22d55', + longitude: '0' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false coords: 0, 11e', function() { + this.subject.config = { + latitude: '0', + longitude: '11e' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false when latitude is empty', function() { + this.subject.config = { + latitude: '', + longitude: '0' + }; + assert.isFalse(this.subject.validate()); + }); + + test('should validate false when longitude is empty', function() { + this.subject.config = { + latitude: '0', + longitude: '' + }; + assert.isFalse(this.subject.validate()); + }); +}); diff --git a/apps/privacy-panel/test/unit/html_helper.js b/apps/privacy-panel/test/unit/html_helper.js new file mode 100644 index 000000000000..566c6e0f562c --- /dev/null +++ b/apps/privacy-panel/test/unit/html_helper.js @@ -0,0 +1,57 @@ +define(function() { + 'use strict'; + + var parser = new DOMParser(); + + function LoadHTMLHelper() { + this.templates = {}; + } + + LoadHTMLHelper.prototype = { + + get: function(filename) { + if (!(filename in this.templates)) { + this.requestHTML(filename); + } + return this.templates[filename]; + }, + + requestHTML: function(filename) { + var request = new XMLHttpRequest(); + request.open('GET', filename, false); + request.onerror = function() { + throw new Error('Unable to load template: ' + filename); + }; + request.send(); + + if (request.status !== 200) { + request.onerror(); + } + + var result = request.response + .replace(/<\/?template>/i, '') + .replace('element', 'section'); + + var html = parser.parseFromString(result, 'text/html'); + if (!html) { + throw new Error('Unable to parse ' + filename + ' for body HTML'); + } + + var name = html.querySelector('section').getAttribute('name'); + html.querySelector('section').setAttribute('id', name); + + this.removeElementsFromHTML(html, 'script'); + this.templates[filename] = html.body; + }, + + removeElementsFromHTML: function(html, selector) { + var elements = html.querySelectorAll(selector); + Array.prototype.forEach.call(elements, function(el) { + el.parentNode.removeChild(el); + }); + } + + }; + + return new LoadHTMLHelper(); +}); \ No newline at end of file diff --git a/apps/privacy-panel/test/unit/mocks/mock_async_storage.js b/apps/privacy-panel/test/unit/mocks/mock_async_storage.js new file mode 100644 index 000000000000..3363585aec1c --- /dev/null +++ b/apps/privacy-panel/test/unit/mocks/mock_async_storage.js @@ -0,0 +1,18 @@ +define(function() { + 'use strict'; + + var MockasyncStorage = { + data: {}, + getItem: function(key, callback) { + callback(this.data[key]); + }, + setItem: function(key, value, callback) { + this.data[key] = value; + callback(); + } + }; + + return MockasyncStorage; +}); + + diff --git a/apps/privacy-panel/test/unit/mocks/mock_commands.js b/apps/privacy-panel/test/unit/mocks/mock_commands.js new file mode 100644 index 000000000000..4cb9398116bf --- /dev/null +++ b/apps/privacy-panel/test/unit/mocks/mock_commands.js @@ -0,0 +1,78 @@ +/** + * Command module to handle lock, ring, locate features. + * + * @module Commands + * @return {Object} + */ +define([ + 'shared/settings_listener', + 'shared/settings_helper' +], + +function(SettingsListener, SettingsHelper) { + 'use strict'; + + var Commands = { + TRACK_UPDATE_INTERVAL_MS: 10000, + + _ringer: null, + + _lockscreenEnabled: false, + + _lockscreenPassCodeEnabled: false, + + _geolocationEnabled: false, + + init: function fmdc_init() { + var self = this; + SettingsListener.observe('lockscreen.enabled', false, function(value) { + self._lockscreenEnabled = value; + }); + + SettingsListener.observe('lockscreen.passcode-lock.enabled', false, + function(value) { + self._lockscreenPassCodeEnabled = value; + } + ); + + SettingsListener.observe('geolocation.enabled', false, function(value) { + self._geolocationEnabled = value; + }); + }, + + invokeCommand: function fmdc_get_command(name, args) { + this._commands[name].apply(this, args); + }, + + deviceHasPasscode: function fmdc_device_has_passcode() { + return !!(this._lockscreenEnabled && this._lockscreenPassCodeEnabled); + }, + + _ringTimeoutId: null, + + _commands: { + locate: function fmdc_track(duration, reply) { + reply = reply || function() {}; + reply(true, { + coords: { + latitude: 51, + longitude: 13 + } + }); + }, + + lock: function fmdc_lock(message, passcode, reply) { + reply = reply || function() {}; + reply(true); + }, + + ring: function fmdc_ring(duration, reply) { + reply = reply || function() {}; + reply(true); + } + } + }; + + return Commands; + +}); diff --git a/apps/privacy-panel/test/unit/mocks/mock_passphrase.js b/apps/privacy-panel/test/unit/mocks/mock_passphrase.js new file mode 100644 index 000000000000..e9da16518761 --- /dev/null +++ b/apps/privacy-panel/test/unit/mocks/mock_passphrase.js @@ -0,0 +1,54 @@ +/** + * PassPhrase storage helper. + * + * @module PassPhrase + * @return {Object} + */ +define([ + 'shared/async_storage' +], + +function(asyncStorage) { + 'use strict'; + + var data = {}; + + function result(value) { + return { + then: function(callback) { + callback(value); + } + }; + } + + function PassPhrase(macDest, saltDest) { + this.macDest = macDest; + this.saltDest = saltDest; + } + + PassPhrase.prototype = { + + exists: function() { + return result(!!data[this.macDest]); + }, + + verify: function(password) { + return result(data[this.macDest] === password); + }, + + change: function(password) { + data[this.macDest] = password; + return result(password); + }, + + clear: function() { + data[this.macDest] = null; + return result(); + } + + }; + + + return PassPhrase; + +}); diff --git a/apps/privacy-panel/test/unit/rpp/auth_test.js b/apps/privacy-panel/test/unit/rpp/auth_test.js new file mode 100644 index 000000000000..6c1feea53dba --- /dev/null +++ b/apps/privacy-panel/test/unit/rpp/auth_test.js @@ -0,0 +1,128 @@ +'use strict'; + +var realMozSettings, htmlHelper; + +suite('RPP Auth panels', function() { + suiteSetup(function(done) { + require([ + 'html_helper', + 'mocks/mock_navigator_moz_settings' + ], + + function(html, mozSettings) { + htmlHelper = html; + realMozSettings = navigator.mozSettings; + navigator.mozSettings = mozSettings; + done(); + }); + }); + + setup(function(done) { + require(['rpp/auth'], authPanel => { + var test; + + this.subject = authPanel; + this.mainHTML = htmlHelper.get('../../templates/rpp/main.html'); + this.changeHTML = htmlHelper.get('../../templates/rpp/change_pass.html'); + this.featuresHTML = htmlHelper.get('../../templates/rpp/features.html'); + + test = document.getElementById('test'); + test.appendChild(this.mainHTML); + test.appendChild(this.changeHTML); + test.appendChild(this.featuresHTML); + + this.subject.init(); + + this.randomizeString = function(length) { + var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + + 'abcdefghijklmnopqrstuvwxyz'; + var result = ''; + + for (var i = 0; i < length; i++) { + var rnum = Math.floor(Math.random() * chars.length); + result += chars.substring(rnum, rnum + 1); + } + + return result; + }; + + done(); + }); + }); + + suiteTeardown(function() { + navigator.mozSettings = realMozSettings; + }); + + suite('validate passphrase', function() { + + test('should validate given passphrase during register', function() { + var p1 = 'mypass'; + var p2 = 'mypass'; + var result = this.subject.comparePasswords(p1, p2); + assert.equal(result, ''); + }); + + test('should result with error (different passwords)', function() { + var p1 = 'mypass1'; + var p2 = 'mypass2'; + var result = this.subject.comparePasswords(p1, p2); + assert.equal(result, 'passphrase-different'); + }); + + test('should result with error (invalid chars)', function() { + var p1 = 'mypass1$%#'; + var p2 = 'mypass2'; + var result = this.subject.comparePasswords(p1, p2); + assert.equal(result, 'passphrase-invalid'); + }); + + test('should result with error (empty password)', function() { + var p1 = ''; + var p2 = ''; + var result = this.subject.comparePasswords(p1, p2); + assert.equal(result, 'passphrase-empty'); + }); + + test('should result with error (password is too long)', function() { + var p1 = this.randomizeString(101); + var p2 = 'test'; + var result = this.subject.comparePasswords(p1, p2); + assert.equal(result, 'passphrase-too-long'); + }); + + }); + + suite('validate pin', function() { + + test('should validate given pin during change request', function() { + var p1 = '1337'; + var p2 = '1337'; + var result = this.subject.comparePINs(p1, p2); + assert.equal(result, ''); + }); + + test('should result with error (different passwords)', function() { + var p1 = '1337'; + var p2 = '2578'; + var result = this.subject.comparePINs(p1, p2); + assert.equal(result, 'pin-different'); + }); + + test('should result with error (invalid chars)', function() { + var p1 = 'pin'; + var p2 = '1234'; + var result = this.subject.comparePINs(p1, p2); + assert.equal(result, 'pin-invalid'); + }); + + test('should result with error (empty password)', function() { + var p1 = ''; + var p2 = ''; + var result = this.subject.comparePINs(p1, p2); + assert.equal(result, 'pin-empty'); + }); + + }); + +}); diff --git a/apps/privacy-panel/test/unit/rpp/passphrase_test.js b/apps/privacy-panel/test/unit/rpp/passphrase_test.js new file mode 100644 index 000000000000..db79380ec2a8 --- /dev/null +++ b/apps/privacy-panel/test/unit/rpp/passphrase_test.js @@ -0,0 +1,45 @@ +'use strict'; + +suite('RPP PassPhrase module', function() { + setup(function(done) { + require(['rpp/passphrase'], PassPhrase => { + this.subject = new PassPhrase('salt', 'mac'); + done(); + }); + }); + + test('should notify us about empty passphrase', function(done) { + this.subject.exists().then(function(status) { + assert.isFalse(status); + done(); + }); + }); + + test('should give us ability to save passphrase', function(done) { + this.subject.change('mypass').then(function(value) { + assert.isNotNull(value); + done(); + }); + }); + + test('should give us flag that passphrase exists', function(done) { + this.subject.exists().then(function(status) { + assert.isTrue(status); + done(); + }); + }); + + test('should allow us to verify our passphrase', function(done) { + this.subject.verify('mypass').then(function(status) { + assert.isTrue(status); + done(); + }); + }); + + test('should not allow us to verify random passphrase', function(done) { + this.subject.verify('random').then(function(status) { + assert.isFalse(status); + done(); + }); + }); +}); diff --git a/apps/privacy-panel/test/unit/setup.js b/apps/privacy-panel/test/unit/setup.js new file mode 100644 index 000000000000..4d40b325216f --- /dev/null +++ b/apps/privacy-panel/test/unit/setup.js @@ -0,0 +1,43 @@ +'use strict'; + +requireApp('privacy-panel/js/vendor/alameda.js', () => { + this.require = requirejs.config({ + baseUrl: '/js', + paths: { + shared: '../shared/js', + mocks: '../shared/test/unit/mocks', + mymocks: '/test/unit/mocks', + html_helper: '../test/unit/html_helper' + }, + shim: { + 'mocks/mock_navigator_moz_apps': { + exports: 'MockNavigatormozApps' + }, + 'mocks/mock_navigator_moz_settings': { + exports: 'MockNavigatorSettings' + }, + 'shared/settings_listener': { + exports: 'SettingsListener' + }, + 'shared/settings_helper': { + exports: 'SettingsHelper' + }, + 'mocks/mock_navigator_moz_set_message_handler': { + exports: 'MockNavigatormozSetMessageHandler' + }, + 'shared/settings_url': { + exports: 'SettingsURL' + }, + 'mocks/mock_l10n': { + exports: 'MockL10n' + } + }, + urlArgs: 'cache_bust=' + Date.now(), + map: { + '*': { + 'shared/async_storage': 'mymocks/mock_async_storage', + 'sms/commands': 'mymocks/mock_commands' + } + } + }); +}); diff --git a/apps/privacy-panel/test/unit/sms/main_test.js b/apps/privacy-panel/test/unit/sms/main_test.js new file mode 100644 index 000000000000..f7a9d5a53351 --- /dev/null +++ b/apps/privacy-panel/test/unit/sms/main_test.js @@ -0,0 +1,193 @@ +'use strict'; + +var passphrase, realMozSettings, realMozSetMessageHandler, realL10n; + +suite('SMS Main', function() { + suiteSetup(function(done) { + require([ + 'mymocks/mock_passphrase', + 'mocks/mock_navigator_moz_settings', + 'mocks/mock_navigator_moz_set_message_handler', + 'mocks/mock_l10n' + ], + function(PassPhrase, mozSettings, mozSetMessageHandler, MockL10n) { + realMozSettings = navigator.mozSettings; + navigator.mozSettings = mozSettings; + + realL10n = navigator.mozL10n; + navigator.mozL10n = MockL10n; + + realMozSetMessageHandler = navigator.mozSetMessageHandler; + navigator.mozSetMessageHandler = mozSetMessageHandler; + navigator.mozSetMessageHandler.mSetup(); + + // create passphrase instance + passphrase = new PassPhrase('rppmac', 'rppsalt'); + passphrase.change('mypass').then(function() { + done(); + }); + }); + }); + + setup(function(done) { + var customRequire = requirejs.config({ + map: { + '*': { + 'rpp/passphrase': 'mymocks/mock_passphrase', + 'sms/commands': 'mymocks/mock_commands' + } + } + }); + customRequire(['sms/main'], RppSMSHandler => { + this.subject = RppSMSHandler; + this.subject.init(); + + this.sandbox = sinon.sandbox.create(); + + this.fakeSMS = function(message) { + navigator.mozSetMessageHandler.mTrigger('sms-received', { + body: message, + sender: 123456789 + }); + }; + + done(); + }); + }); + + suiteTeardown(function() { + navigator.mozSetMessageHandler.mTeardown(); + navigator.mozSetMessageHandler = realMozSetMessageHandler; + navigator.mozSettings = realMozSettings; + navigator.mozL10n = realL10n; + }); + + suite('Handling SMS response', function() { + + setup(function() { + this.sandbox.spy(this.subject, '_ring'); + this.sandbox.spy(this.subject, '_lock'); + this.sandbox.spy(this.subject, '_locate'); + this.sandbox.spy(this.subject, '_sendSMS'); + + navigator.mozSettings.createLock().set({ + 'lockscreen.enabled': false, + 'lockscreen.passcode-lock.enabled': false, + 'rpp.ring.enabled': false, + 'rpp.lock.enabled': false, + 'rpp.locate.enabled': false + }); + }); + + teardown(function() { + this.sandbox.restore(); + }); + + test('should do nothing, bad command was send', function() { + this.fakeSMS('rpp ringy mypass'); + + sinon.assert.notCalled(this.subject._ring); + sinon.assert.notCalled(this.subject._lock); + sinon.assert.notCalled(this.subject._locate); + }); + + test('should do nothing, bad sms was send', function() { + this.fakeSMS('esrpp ringy mypass'); + + sinon.assert.notCalled(this.subject._ring); + sinon.assert.notCalled(this.subject._lock); + sinon.assert.notCalled(this.subject._locate); + }); + + test('should do nothing, ring feature is turned off', function() { + this.fakeSMS('rpp ring mypass'); + + sinon.assert.notCalled(this.subject._ring); + sinon.assert.notCalled(this.subject._lock); + sinon.assert.notCalled(this.subject._locate); + }); + + test('should do nothing, lock feature is disabled', function() { + this.fakeSMS('rpp lock mypass'); + + sinon.assert.notCalled(this.subject._ring); + sinon.assert.notCalled(this.subject._lock); + sinon.assert.notCalled(this.subject._locate); + }); + + test('should do nothing, locate feature is disabled', function() { + this.fakeSMS('rpp locate mypass'); + + sinon.assert.notCalled(this.subject._ring); + sinon.assert.notCalled(this.subject._lock); + sinon.assert.notCalled(this.subject._locate); + }); + + test('should do nothing, bad passphrase was send', function() { + this.fakeSMS('rpp ring badpass'); + + navigator.mozSettings.createLock().set({ + 'lockscreen.enabled': true, + 'lockscreen.passcode-lock.enabled': true, + 'rpp.ring.enabled': true + }); + + sinon.assert.notCalled(this.subject._ring); + sinon.assert.notCalled(this.subject._lock); + sinon.assert.notCalled(this.subject._locate); + }); + + test('should ring device, ring feature enabled', function() { + navigator.mozSettings.createLock().set({ + 'lockscreen.enabled': true, + 'lockscreen.passcode-lock.enabled': true, + 'rpp.ring.enabled': true + }); + + this.fakeSMS('rpp ring mypass'); + + sinon.assert.called(this.subject._ring); + sinon.assert.notCalled(this.subject._lock); + sinon.assert.notCalled(this.subject._locate); + sinon.assert.calledWith(this.subject._sendSMS, 123456789, 'sms-ring'); + }); + + test('should lock device, lock feature enabled', function() { + navigator.mozSettings.createLock().set({ + 'lockscreen.enabled': true, + 'lockscreen.passcode-lock.enabled': true, + 'rpp.lock.enabled': true + }); + + this.fakeSMS('rpp lock mypass'); + + sinon.assert.notCalled(this.subject._ring); + sinon.assert.called(this.subject._lock); + sinon.assert.notCalled(this.subject._locate); + sinon.assert.calledWith(this.subject._sendSMS, 123456789, 'sms-lock'); + }); + + test('should locate device, locate feature enabled', function() { + navigator.mozSettings.createLock().set({ + 'lockscreen.enabled': true, + 'lockscreen.passcode-lock.enabled': true, + 'rpp.locate.enabled': true + }); + + this.fakeSMS('rpp locate mypass'); + + sinon.assert.notCalled(this.subject._ring); + sinon.assert.notCalled(this.subject._lock); + sinon.assert.called(this.subject._locate); + sinon.assert.calledWith(this.subject._sendSMS, 123456789, { + id: 'sms-locate', + args: { + latitude: 51, + longitude: 13 + } + }); + }); + + }); + +}); diff --git a/apps/settings/elements/root.html b/apps/settings/elements/root.html index d7b20a3ed05e..6d2422def7cc 100644 --- a/apps/settings/elements/root.html +++ b/apps/settings/elements/root.html @@ -172,6 +172,12 @@

Privacy & Security

Browsing Privacy +
@@ -264,7 +270,7 @@

Enable USB storage?

- + diff --git a/apps/settings/js/panels/root/panel.js b/apps/settings/js/panels/root/panel.js index fc81fd52e1a9..e7194c5999b6 100644 --- a/apps/settings/js/panels/root/panel.js +++ b/apps/settings/js/panels/root/panel.js @@ -16,6 +16,7 @@ define(function(require) { var AirplaneModeItem = require('panels/root/airplane_mode_item'); var ThemesItem = require('panels/root/themes_item'); var HomescreenItem = require('panels/root/homescreen_item'); + var PrivacyPanelItem = require('panels/root/privacy_panel_item'); return function ctor_root_panel() { var root = Root(); @@ -32,6 +33,7 @@ define(function(require) { var airplaneModeItem; var themesItem; var homescreenItem; + var privacyPanelItem; return SettingsPanel({ onInit: function rp_onInit(panel) { @@ -65,6 +67,10 @@ define(function(require) { ThemesItem(panel.querySelector('.themes-section')); homescreenItem = HomescreenItem(panel.querySelector('#homescreens-section')); + privacyPanelItem = PrivacyPanelItem({ + element: panel.querySelector('.privacy-panel-item'), + link: panel.querySelector('.privacy-panel-item a') + }); }, onBeforeShow: function rp_onBeforeShow() { bluetoothItem.enabled = true; @@ -78,6 +84,7 @@ define(function(require) { simSecurityItem.enabled = true; airplaneModeItem.enabled = true; themesItem.enabled = true; + privacyPanelItem.enabled = true; }, onShow: function rp_onShow() { homescreenItem.enabled = true; @@ -95,6 +102,7 @@ define(function(require) { airplaneModeItem.enabled = false; themesItem.enabled = false; homescreenItem.enabled = false; + privacyPanelItem.enabled = false; } }); }; diff --git a/apps/settings/js/panels/root/privacy_panel_item.js b/apps/settings/js/panels/root/privacy_panel_item.js new file mode 100644 index 000000000000..4b310a5e6020 --- /dev/null +++ b/apps/settings/js/panels/root/privacy_panel_item.js @@ -0,0 +1,116 @@ +/** + * PrivacyPanelItem provides the transition to Privacy Panel app. + * + * @module PrivacyPanelItem + */ + +define(function(require) { + 'use strict'; + + var AppsCache = require('modules/apps_cache'); + + function PrivacyPanelItem(args) { + this._element = args.element; + this._link = args.link; + this._app = null; + + this._privacyPanelManifestURL = document.location.protocol + + '//privacy-panel.gaiamobile.org' + + (location.port ? (':' + location.port) : '') + '/manifest.webapp'; + + this._getApp(); + + this._element.addEventListener('click', this._launch.bind(this)); + } + + PrivacyPanelItem.prototype = { + + /** + * Set current status of privacyPanelItem + * + * @access public + * @param {Boolean} enabled + * @memberOf PrivacyPanelItem + */ + set enabled(enabled) { + if (this._enabled === enabled) { + return; + } else { + this._enabled = enabled; + if (this._enabled) { + this._blurLink(); + } + } + }, + + /** + * Get current status of privacyPanelItem + * + * @access public + * @memberOf PrivacyPanelItem + */ + get enabled() { + return this._enabled; + }, + + /** + * Search from privacy-panel app and grab it's instance. + * @memberOf PrivacyPanelItem + */ + _getApp: function pp_getApp() { + return AppsCache.apps().then(function(apps) { + var i, app; + for (i = 0; i < apps.length; i++) { + app = apps[i]; + if (app.manifestURL === this._privacyPanelManifestURL) { + this._app = app; + this._element.removeAttribute('hidden'); + return; + } + } + }.bind(this)); + }, + + /** + * Launch Privacy Panel app. + * + * @param {Event} event + * @memberOf PrivacyPanelItem + */ + _launch: function pp_launch(event) { + // Stop propagation & prevent default not to block other settings events. + event.stopImmediatePropagation(); + event.preventDefault(); + + if (this._app) { + // Let privacy-panel app know that we launched it from settings + // so the app can show us a back button pointing to settings app. + var flag = navigator.mozSettings.createLock().set({ + 'privacypanel.launched.by.settings': true + }); + flag.onsuccess = function() { + this._app.launch(); + }.bind(this); + flag.onerror = function() { + console.error('Problem with launching Privacy Panel'); + alert('Problem with launching Privacy Panel'); + }; + } else { + alert(navigator.mozL10n.get('no-privacy-panel')); + } + }, + + /** + * Blur link. + * + * @memberOf PrivacyPanelItem + */ + _blurLink: function pp_blurLink() { + this._link.blur(); + } + }; + + return function ctor_privacyPanelItem(element) { + return new PrivacyPanelItem(element); + }; +}); diff --git a/apps/settings/locales/settings.en-US.properties b/apps/settings/locales/settings.en-US.properties index abd2fe912bd0..f55ef3f1429e 100644 --- a/apps/settings/locales/settings.en-US.properties +++ b/apps/settings/locales/settings.en-US.properties @@ -894,6 +894,10 @@ clear-cookies-and-stored-data=Clear cookies and stored data confirm-clear-cookies-and-stored-data=Clear cookies and other data stored by sites on this device? clear=Clear +# Security :: Privacy Panel +privacy-panel=Privacy Panel +no-privacy-panel=No privacy panel app found + #=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=# diff --git a/apps/settings/test/unit/panels/root/privacy_panel_item_test.js b/apps/settings/test/unit/panels/root/privacy_panel_item_test.js new file mode 100644 index 000000000000..3db323f37ee1 --- /dev/null +++ b/apps/settings/test/unit/panels/root/privacy_panel_item_test.js @@ -0,0 +1,71 @@ +'use strict'; + +suite('PrivacyPanelItem', function() { + suiteSetup(function(done) { + var modules = [ + 'panels/root/privacy_panel_item', + 'unit/mock_apps_cache', + 'shared_mocks/mock_navigator_moz_settings', + 'shared_mocks/mock_l10n' + ]; + + var maps = { + '*': { + 'modules/apps_cache': 'unit/mock_apps_cache' + } + }; + + testRequire(modules, maps, function(PrivacyPanelItem, MockAppsCache, + MockNavigatorSettings, MockL10n) { + navigator.mozSettings = MockNavigatorSettings; + navigator.mozL10n = MockL10n; + + MockAppsCache._apps = [{ + manifestURL: document.location.protocol + + '//privacy-panel.gaiamobile.org' + + (location.port ? (':' + location.port) : '') + '/manifest.webapp', + launch: function() {} + }]; + + this.PrivacyPanelItem = PrivacyPanelItem; + done(); + }.bind(this)); + }); + + setup(function(done) { + this.element = document.createElement('div'); + this.element.setAttribute('hidden', 'hidden'); + this.link = document.createElement('a'); + this.subject = this.PrivacyPanelItem({ + element: this.element, + link: this.link + }); + + // lets wait till Promise resolve privacy-panel app. + this.subject._getApp().then(done, done); + }); + + test('search for privacy-panel app (_getApp method)', function(done) { + // lets wait till Promise resolve privacy-panel app + this.subject._getApp().then( + function() { + assert.isNotNull(this.subject._app); + assert.isDefined(this.subject._app.launch); + }.bind(this), + function() { + // This function does not reject. + assert.isTrue(false); + } + ).then(done, done); + }); + + test('launch privacy panel app (_launch method)', function(done) { + navigator.mozSettings.addObserver('privacypanel.launched.by.settings', + function(value) { + assert.isTrue(value.settingValue); + done(); + } + ); + this.element.click(); + }); +}); diff --git a/build/config/phone/apps-production.list b/build/config/phone/apps-production.list index d6b9e0210bbb..e6e8077ca9b8 100644 --- a/build/config/phone/apps-production.list +++ b/build/config/phone/apps-production.list @@ -33,3 +33,4 @@ apps/video apps/wallpaper apps/wappush outoftree_apps/* +apps/privacy-panel