Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 <include src="../uber/uber_utils.js">
      6 <include src="extension_code.js">
      7 <include src="extension_commands_overlay.js">
      8 <include src="extension_error_overlay.js">
      9 <include src="extension_focus_manager.js">
     10 <include src="extension_list.js">
     11 <include src="pack_extension_overlay.js">
     12 <include src="extension_loader.js">
     13 <include src="extension_options_overlay.js">
     14 
     15 <if expr="chromeos">
     16 <include src="chromeos/kiosk_apps.js">
     17 </if>
     18 
     19 /**
     20  * The type of the extension data object. The definition is based on
     21  * chrome/browser/ui/webui/extensions/extension_settings_handler.cc:
     22  *     ExtensionSettingsHandler::HandleRequestExtensionsData()
     23  * @typedef {{developerMode: boolean,
     24  *            extensions: Array,
     25  *            incognitoAvailable: boolean,
     26  *            loadUnpackedDisabled: boolean,
     27  *            profileIsSupervised: boolean,
     28  *            promoteAppsDevTools: boolean}}
     29  */
     30 var ExtensionDataResponse;
     31 
     32 // Used for observing function of the backend datasource for this page by
     33 // tests.
     34 var webuiResponded = false;
     35 
     36 cr.define('extensions', function() {
     37   var ExtensionsList = options.ExtensionsList;
     38 
     39   // Implements the DragWrapper handler interface.
     40   var dragWrapperHandler = {
     41     /** @override */
     42     shouldAcceptDrag: function(e) {
     43       // We can't access filenames during the 'dragenter' event, so we have to
     44       // wait until 'drop' to decide whether to do something with the file or
     45       // not.
     46       // See: http://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#concept-dnd-p
     47       return (e.dataTransfer.types &&
     48               e.dataTransfer.types.indexOf('Files') > -1);
     49     },
     50     /** @override */
     51     doDragEnter: function() {
     52       chrome.send('startDrag');
     53       ExtensionSettings.showOverlay(null);
     54       ExtensionSettings.showOverlay($('drop-target-overlay'));
     55     },
     56     /** @override */
     57     doDragLeave: function() {
     58       ExtensionSettings.showOverlay(null);
     59       chrome.send('stopDrag');
     60     },
     61     /** @override */
     62     doDragOver: function(e) {
     63       e.preventDefault();
     64     },
     65     /** @override */
     66     doDrop: function(e) {
     67       ExtensionSettings.showOverlay(null);
     68       if (e.dataTransfer.files.length != 1)
     69         return;
     70 
     71       var toSend = null;
     72       // Files lack a check if they're a directory, but we can find out through
     73       // its item entry.
     74       for (var i = 0; i < e.dataTransfer.items.length; ++i) {
     75         if (e.dataTransfer.items[i].kind == 'file' &&
     76             e.dataTransfer.items[i].webkitGetAsEntry().isDirectory) {
     77           toSend = 'installDroppedDirectory';
     78           break;
     79         }
     80       }
     81       // Only process files that look like extensions. Other files should
     82       // navigate the browser normally.
     83       if (!toSend &&
     84           /\.(crx|user\.js|zip)$/i.test(e.dataTransfer.files[0].name)) {
     85         toSend = 'installDroppedFile';
     86       }
     87 
     88       if (toSend) {
     89         e.preventDefault();
     90         chrome.send(toSend);
     91       }
     92     }
     93   };
     94 
     95   /**
     96    * ExtensionSettings class
     97    * @class
     98    */
     99   function ExtensionSettings() {}
    100 
    101   cr.addSingletonGetter(ExtensionSettings);
    102 
    103   ExtensionSettings.prototype = {
    104     __proto__: HTMLDivElement.prototype,
    105 
    106     /**
    107      * Whether or not to try to display the Apps Developer Tools promotion.
    108      * @type {boolean}
    109      * @private
    110      */
    111     displayPromo_: false,
    112 
    113     /**
    114      * Perform initial setup.
    115      */
    116     initialize: function() {
    117       uber.onContentFrameLoaded();
    118       cr.ui.FocusOutlineManager.forDocument(document);
    119       measureCheckboxStrings();
    120 
    121       // Set the title.
    122       uber.setTitle(loadTimeData.getString('extensionSettings'));
    123 
    124       // This will request the data to show on the page and will get a response
    125       // back in returnExtensionsData.
    126       chrome.send('extensionSettingsRequestExtensionsData');
    127 
    128       var extensionLoader = extensions.ExtensionLoader.getInstance();
    129 
    130       $('toggle-dev-on').addEventListener('change',
    131           this.handleToggleDevMode_.bind(this));
    132       $('dev-controls').addEventListener('webkitTransitionEnd',
    133           this.handleDevControlsTransitionEnd_.bind(this));
    134 
    135       // Set up the three dev mode buttons (load unpacked, pack and update).
    136       $('load-unpacked').addEventListener('click', function(e) {
    137           extensionLoader.loadUnpacked();
    138       });
    139       $('pack-extension').addEventListener('click',
    140           this.handlePackExtension_.bind(this));
    141       $('update-extensions-now').addEventListener('click',
    142           this.handleUpdateExtensionNow_.bind(this));
    143 
    144       // Set up the close dialog for the apps developer tools promo.
    145       $('apps-developer-tools-promo').querySelector('.close-button').
    146           addEventListener('click', function(e) {
    147         this.displayPromo_ = false;
    148         this.updatePromoVisibility_();
    149         chrome.send('extensionSettingsDismissADTPromo');
    150       }.bind(this));
    151 
    152       if (!loadTimeData.getBoolean('offStoreInstallEnabled')) {
    153         this.dragWrapper_ = new cr.ui.DragWrapper(document.documentElement,
    154                                                   dragWrapperHandler);
    155       }
    156 
    157       extensions.PackExtensionOverlay.getInstance().initializePage();
    158 
    159       // Hook up the configure commands link to the overlay.
    160       var link = document.querySelector('.extension-commands-config');
    161       link.addEventListener('click',
    162           this.handleExtensionCommandsConfig_.bind(this));
    163 
    164       // Initialize the Commands overlay.
    165       extensions.ExtensionCommandsOverlay.getInstance().initializePage();
    166 
    167       extensions.ExtensionErrorOverlay.getInstance().initializePage(
    168           extensions.ExtensionSettings.showOverlay);
    169 
    170       extensions.ExtensionOptionsOverlay.getInstance().initializePage(
    171           extensions.ExtensionSettings.showOverlay);
    172 
    173       // Initialize the kiosk overlay.
    174       if (cr.isChromeOS) {
    175         var kioskOverlay = extensions.KioskAppsOverlay.getInstance();
    176         kioskOverlay.initialize();
    177 
    178         $('add-kiosk-app').addEventListener('click', function() {
    179           ExtensionSettings.showOverlay($('kiosk-apps-page'));
    180           kioskOverlay.didShowPage();
    181         });
    182 
    183         extensions.KioskDisableBailoutConfirm.getInstance().initialize();
    184       }
    185 
    186       cr.ui.overlay.setupOverlay($('drop-target-overlay'));
    187       cr.ui.overlay.globalInitialization();
    188 
    189       extensions.ExtensionFocusManager.getInstance().initialize();
    190 
    191       var path = document.location.pathname;
    192       if (path.length > 1) {
    193         // Skip starting slash and remove trailing slash (if any).
    194         var overlayName = path.slice(1).replace(/\/$/, '');
    195         if (overlayName == 'configureCommands')
    196           this.showExtensionCommandsConfigUi_();
    197       }
    198 
    199       preventDefaultOnPoundLinkClicks();  // From webui/js/util.js.
    200     },
    201 
    202     /**
    203      * Updates the Chrome Apps and Extensions Developer Tools promotion's
    204      * visibility.
    205      * @private
    206      */
    207     updatePromoVisibility_: function() {
    208       var extensionSettings = $('extension-settings');
    209       var visible = extensionSettings.classList.contains('dev-mode') &&
    210                     this.displayPromo_;
    211 
    212       var adtPromo = $('apps-developer-tools-promo');
    213       var controls = adtPromo.querySelectorAll('a, button');
    214       Array.prototype.forEach.call(controls, function(control) {
    215         control[visible ? 'removeAttribute' : 'setAttribute']('tabindex', '-1');
    216       });
    217 
    218       adtPromo.setAttribute('aria-hidden', !visible);
    219       extensionSettings.classList.toggle('adt-promo', visible);
    220     },
    221 
    222     /**
    223      * Handles the Pack Extension button.
    224      * @param {Event} e Change event.
    225      * @private
    226      */
    227     handlePackExtension_: function(e) {
    228       ExtensionSettings.showOverlay($('pack-extension-overlay'));
    229       chrome.send('metricsHandler:recordAction', ['Options_PackExtension']);
    230     },
    231 
    232     /**
    233      * Shows the Extension Commands configuration UI.
    234      * @param {Event} e Change event.
    235      * @private
    236      */
    237     showExtensionCommandsConfigUi_: function(e) {
    238       ExtensionSettings.showOverlay($('extension-commands-overlay'));
    239       chrome.send('metricsHandler:recordAction',
    240                   ['Options_ExtensionCommands']);
    241     },
    242 
    243     /**
    244      * Handles the Configure (Extension) Commands link.
    245      * @param {Event} e Change event.
    246      * @private
    247      */
    248     handleExtensionCommandsConfig_: function(e) {
    249       this.showExtensionCommandsConfigUi_();
    250     },
    251 
    252     /**
    253      * Handles the Update Extension Now button.
    254      * @param {Event} e Change event.
    255      * @private
    256      */
    257     handleUpdateExtensionNow_: function(e) {
    258       chrome.send('extensionSettingsAutoupdate');
    259     },
    260 
    261     /**
    262      * Handles the Toggle Dev Mode button.
    263      * @param {Event} e Change event.
    264      * @private
    265      */
    266     handleToggleDevMode_: function(e) {
    267       if ($('toggle-dev-on').checked) {
    268         $('dev-controls').hidden = false;
    269         window.setTimeout(function() {
    270           $('extension-settings').classList.add('dev-mode');
    271         }, 0);
    272       } else {
    273         $('extension-settings').classList.remove('dev-mode');
    274       }
    275       window.setTimeout(this.updatePromoVisibility_.bind(this), 0);
    276 
    277       chrome.send('extensionSettingsToggleDeveloperMode');
    278     },
    279 
    280     /**
    281      * Called when a transition has ended for #dev-controls.
    282      * @param {Event} e webkitTransitionEnd event.
    283      * @private
    284      */
    285     handleDevControlsTransitionEnd_: function(e) {
    286       if (e.propertyName == 'height' &&
    287           !$('extension-settings').classList.contains('dev-mode')) {
    288         $('dev-controls').hidden = true;
    289       }
    290     },
    291   };
    292 
    293   /**
    294    * Called by the dom_ui_ to re-populate the page with data representing
    295    * the current state of installed extensions.
    296    * @param {ExtensionDataResponse} extensionsData
    297    */
    298   ExtensionSettings.returnExtensionsData = function(extensionsData) {
    299     // We can get called many times in short order, thus we need to
    300     // be careful to remove the 'finished loading' timeout.
    301     if (this.loadingTimeout_)
    302       window.clearTimeout(this.loadingTimeout_);
    303     document.documentElement.classList.add('loading');
    304     this.loadingTimeout_ = window.setTimeout(function() {
    305       document.documentElement.classList.remove('loading');
    306     }, 0);
    307 
    308     webuiResponded = true;
    309 
    310     if (extensionsData.extensions.length > 0) {
    311       // Enforce order specified in the data or (if equal) then sort by
    312       // extension name (case-insensitive) followed by their ID (in the case
    313       // where extensions have the same name).
    314       extensionsData.extensions.sort(function(a, b) {
    315         function compare(x, y) {
    316           return x < y ? -1 : (x > y ? 1 : 0);
    317         }
    318         return compare(a.order, b.order) ||
    319                compare(a.name.toLowerCase(), b.name.toLowerCase()) ||
    320                compare(a.id, b.id);
    321       });
    322     }
    323 
    324     var pageDiv = $('extension-settings');
    325     var marginTop = 0;
    326     if (extensionsData.profileIsSupervised) {
    327       pageDiv.classList.add('profile-is-supervised');
    328     } else {
    329       pageDiv.classList.remove('profile-is-supervised');
    330     }
    331     if (extensionsData.profileIsSupervised) {
    332       pageDiv.classList.add('showing-banner');
    333       $('toggle-dev-on').disabled = true;
    334       marginTop += 45;
    335     } else {
    336       pageDiv.classList.remove('showing-banner');
    337       $('toggle-dev-on').disabled = false;
    338     }
    339 
    340     pageDiv.style.marginTop = marginTop + 'px';
    341 
    342     if (extensionsData.developerMode) {
    343       pageDiv.classList.add('dev-mode');
    344       $('toggle-dev-on').checked = true;
    345       $('dev-controls').hidden = false;
    346     } else {
    347       pageDiv.classList.remove('dev-mode');
    348       $('toggle-dev-on').checked = false;
    349     }
    350 
    351     ExtensionSettings.getInstance().displayPromo_ =
    352         extensionsData.promoteAppsDevTools;
    353     ExtensionSettings.getInstance().updatePromoVisibility_();
    354 
    355     $('load-unpacked').disabled = extensionsData.loadUnpackedDisabled;
    356 
    357     ExtensionsList.prototype.data_ = extensionsData;
    358     var extensionList = $('extension-settings-list');
    359     ExtensionsList.decorate(extensionList);
    360   };
    361 
    362   // Indicate that warning |message| has occured for pack of |crx_path| and
    363   // |pem_path| files.  Ask if user wants override the warning.  Send
    364   // |overrideFlags| to repeated 'pack' call to accomplish the override.
    365   ExtensionSettings.askToOverrideWarning =
    366       function(message, crx_path, pem_path, overrideFlags) {
    367     var closeAlert = function() {
    368       ExtensionSettings.showOverlay(null);
    369     };
    370 
    371     alertOverlay.setValues(
    372         loadTimeData.getString('packExtensionWarningTitle'),
    373         message,
    374         loadTimeData.getString('packExtensionProceedAnyway'),
    375         loadTimeData.getString('cancel'),
    376         function() {
    377           chrome.send('pack', [crx_path, pem_path, overrideFlags]);
    378           closeAlert();
    379         },
    380         closeAlert);
    381     ExtensionSettings.showOverlay($('alertOverlay'));
    382   };
    383 
    384   /**
    385    * Returns the current overlay or null if one does not exist.
    386    * @return {Element} The overlay element.
    387    */
    388   ExtensionSettings.getCurrentOverlay = function() {
    389     return document.querySelector('#overlay .page.showing');
    390   };
    391 
    392   /**
    393    * Sets the given overlay to show. This hides whatever overlay is currently
    394    * showing, if any.
    395    * @param {HTMLElement} node The overlay page to show. If falsey, all overlays
    396    *     are hidden.
    397    */
    398   ExtensionSettings.showOverlay = function(node) {
    399     var pageDiv = $('extension-settings');
    400     if (node) {
    401       pageDiv.style.width = window.getComputedStyle(pageDiv).width;
    402       document.body.classList.add('no-scroll');
    403     } else {
    404       document.body.classList.remove('no-scroll');
    405       pageDiv.style.width = '';
    406     }
    407 
    408     var currentlyShowingOverlay = ExtensionSettings.getCurrentOverlay();
    409     if (currentlyShowingOverlay)
    410       currentlyShowingOverlay.classList.remove('showing');
    411 
    412     if (node)
    413       node.classList.add('showing');
    414 
    415     var pages = document.querySelectorAll('.page');
    416     for (var i = 0; i < pages.length; i++) {
    417       pages[i].setAttribute('aria-hidden', node ? 'true' : 'false');
    418     }
    419 
    420     $('overlay').hidden = !node;
    421     uber.invokeMethodOnParent(node ? 'beginInterceptingEvents' :
    422                                      'stopInterceptingEvents');
    423   };
    424 
    425   /**
    426    * Utility function to find the width of various UI strings and synchronize
    427    * the width of relevant spans. This is crucial for making sure the
    428    * Enable/Enabled checkboxes align, as well as the Developer Mode checkbox.
    429    */
    430   function measureCheckboxStrings() {
    431     var trashWidth = 30;
    432     var measuringDiv = $('font-measuring-div');
    433     measuringDiv.textContent =
    434         loadTimeData.getString('extensionSettingsEnabled');
    435     measuringDiv.className = 'enabled-text';
    436     var pxWidth = measuringDiv.clientWidth + trashWidth;
    437     measuringDiv.textContent =
    438         loadTimeData.getString('extensionSettingsEnable');
    439     measuringDiv.className = 'enable-text';
    440     pxWidth = Math.max(measuringDiv.clientWidth + trashWidth, pxWidth);
    441     measuringDiv.textContent =
    442         loadTimeData.getString('extensionSettingsDeveloperMode');
    443     measuringDiv.className = '';
    444     pxWidth = Math.max(measuringDiv.clientWidth, pxWidth);
    445 
    446     var style = document.createElement('style');
    447     style.type = 'text/css';
    448     style.textContent =
    449         '.enable-checkbox-text {' +
    450         '  min-width: ' + (pxWidth - trashWidth) + 'px;' +
    451         '}' +
    452         '#dev-toggle span {' +
    453         '  min-width: ' + pxWidth + 'px;' +
    454         '}';
    455     document.querySelector('head').appendChild(style);
    456   };
    457 
    458   // Export
    459   return {
    460     ExtensionSettings: ExtensionSettings
    461   };
    462 });
    463 
    464 window.addEventListener('load', function(e) {
    465   extensions.ExtensionSettings.getInstance().initialize();
    466 });
    467