1 // Copyright (c) 2011 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.
6 * @fileoverview This file implements the ProxyFormController class, which
7 * wraps a form element with logic that enables implementation of proxy
10 * @author mkwst@google.com (Mike West)
14 * Wraps the proxy configuration form, binding proper handlers to its various
15 * `change`, `click`, etc. events in order to take appropriate action in
16 * response to user events.
18 * @param {string} id The form's DOM ID.
21 var ProxyFormController = function(id) {
23 * The wrapped form element
27 this.form_ = document.getElementById(id);
29 // Throw an error if the element either doesn't exist, or isn't a form.
31 throw chrome.i18n.getMessage('errorIdNotFound', id);
32 else if (this.form_.nodeName !== 'FORM')
33 throw chrome.i18n.getMessage('errorIdNotForm', id);
36 * Cached references to the `fieldset` groups that define the configuration
37 * options presented to the user.
42 this.configGroups_ = document.querySelectorAll('#' + id + ' > fieldset');
44 this.bindEventHandlers_();
45 this.readCurrentState_();
48 this.handleProxyErrors_();
51 ///////////////////////////////////////////////////////////////////////////////
54 * The proxy types we're capable of handling.
57 ProxyFormController.ProxyTypes = {
61 FIXED: 'fixed_servers',
66 * The window types we're capable of handling.
69 ProxyFormController.WindowTypes = {
75 * The extension's level of control of Chrome's roxy setting
78 ProxyFormController.LevelOfControl = {
79 NOT_CONTROLLABLE: 'not_controllable',
80 OTHER_EXTENSION: 'controlled_by_other_extension',
81 AVAILABLE: 'controllable_by_this_extension',
82 CONTROLLING: 'controlled_by_this_extension'
86 * The response type from 'proxy.settings.get'
88 * @typedef {{value: ProxyConfig,
89 * levelOfControl: ProxyFormController.LevelOfControl}}
91 ProxyFormController.WrappedProxyConfig;
93 ///////////////////////////////////////////////////////////////////////////////
96 * Retrieves proxy settings that have been persisted across restarts.
98 * @return {?ProxyConfig} The persisted proxy configuration, or null if no
99 * value has been persisted.
102 ProxyFormController.getPersistedSettings = function() {
104 if (window.localStorage['proxyConfig'] !== undefined)
105 result = JSON.parse(window.localStorage['proxyConfig']);
106 return result ? result : null;
111 * Persists proxy settings across restarts.
113 * @param {!ProxyConfig} config The proxy config to persist.
116 ProxyFormController.setPersistedSettings = function(config) {
117 window.localStorage['proxyConfig'] = JSON.stringify(config);
120 ///////////////////////////////////////////////////////////////////////////////
122 ProxyFormController.prototype = {
124 * The form's current state.
125 * @type {regular: ?ProxyConfig, incognito: ?ProxyConfig}
128 config_: {regular: null, incognito: null},
131 * Do we have access to incognito mode?
135 isAllowedIncognitoAccess_: false,
138 * @return {string} The PAC file URL (or an empty string).
141 return document.getElementById('autoconfigURL').value;
146 * @param {!string} value The PAC file URL.
149 document.getElementById('autoconfigURL').value = value;
154 * @return {string} The PAC file data (or an empty string).
157 return document.getElementById('autoconfigData').value;
162 * @param {!string} value The PAC file data.
164 set manualPac(value) {
165 document.getElementById('autoconfigData').value = value;
170 * @return {Array.<string>} A list of hostnames that should bypass the proxy.
173 return document.getElementById('bypassList').value.split(/\s*(?:,|^)\s*/m);
178 * @param {?Array.<string>} data A list of hostnames that should bypass
179 * the proxy. If empty, the bypass list is emptied.
181 set bypassList(data) {
184 document.getElementById('bypassList').value = data.join(', ');
189 * @see http://code.google.com/chrome/extensions/trunk/proxy.html
190 * @return {?ProxyServer} An object containing the proxy server host, port,
191 * and scheme. If null, there is no single proxy.
194 var checkbox = document.getElementById('singleProxyForEverything');
195 return checkbox.checked ? this.httpProxy : null;
200 * @see http://code.google.com/chrome/extensions/trunk/proxy.html
201 * @param {?ProxyServer} data An object containing the proxy server host,
202 * port, and scheme. If null, the single proxy checkbox will be unchecked.
204 set singleProxy(data) {
205 var checkbox = document.getElementById('singleProxyForEverything');
206 checkbox.checked = !!data;
209 this.httpProxy = data;
211 if (checkbox.checked)
212 checkbox.parentNode.parentNode.classList.add('single');
214 checkbox.parentNode.parentNode.classList.remove('single');
218 * @return {?ProxyServer} An object containing the proxy server host, port
222 return this.getProxyImpl_('Http');
227 * @param {?ProxyServer} data An object containing the proxy server host,
228 * port, and scheme. If empty, empties the proxy setting.
230 set httpProxy(data) {
231 this.setProxyImpl_('Http', data);
236 * @return {?ProxyServer} An object containing the proxy server host, port
240 return this.getProxyImpl_('Https');
245 * @param {?ProxyServer} data An object containing the proxy server host,
246 * port, and scheme. If empty, empties the proxy setting.
248 set httpsProxy(data) {
249 this.setProxyImpl_('Https', data);
254 * @return {?ProxyServer} An object containing the proxy server host, port
258 return this.getProxyImpl_('Ftp');
263 * @param {?ProxyServer} data An object containing the proxy server host,
264 * port, and scheme. If empty, empties the proxy setting.
267 this.setProxyImpl_('Ftp', data);
272 * @return {?ProxyServer} An object containing the proxy server host, port
275 get fallbackProxy() {
276 return this.getProxyImpl_('Fallback');
281 * @param {?ProxyServer} data An object containing the proxy server host,
282 * port, and scheme. If empty, empties the proxy setting.
284 set fallbackProxy(data) {
285 this.setProxyImpl_('Fallback', data);
290 * @param {string} type The type of proxy that's being set ("Http",
292 * @return {?ProxyServer} An object containing the proxy server host,
296 getProxyImpl_: function(type) {
298 scheme: document.getElementById('proxyScheme' + type).value,
299 host: document.getElementById('proxyHost' + type).value,
300 port: parseInt(document.getElementById('proxyPort' + type).value, 10)
302 return (result.scheme && result.host && result.port) ? result : undefined;
307 * A generic mechanism for setting proxy data.
309 * @see http://code.google.com/chrome/extensions/trunk/proxy.html
310 * @param {string} type The type of proxy that's being set ("Http",
312 * @param {?ProxyServer} data An object containing the proxy server host,
313 * port, and scheme. If empty, empties the proxy setting.
316 setProxyImpl_: function(type, data) {
318 data = {scheme: 'http', host: '', port: ''};
320 document.getElementById('proxyScheme' + type).value = data.scheme;
321 document.getElementById('proxyHost' + type).value = data.host;
322 document.getElementById('proxyPort' + type).value = data.port;
325 ///////////////////////////////////////////////////////////////////////////////
328 * Calls the proxy API to read the current settings, and populates the form
333 readCurrentState_: function() {
334 chrome.extension.isAllowedIncognitoAccess(
335 this.handleIncognitoAccessResponse_.bind(this));
339 * Handles the respnse from `chrome.extension.isAllowedIncognitoAccess`
340 * We can't render the form until we know what our access level is, so
341 * we wait until we have confirmed incognito access levels before
342 * asking for the proxy state.
344 * @param {boolean} state The state of incognito access.
347 handleIncognitoAccessResponse_: function(state) {
348 this.isAllowedIncognitoAccess_ = state;
349 chrome.proxy.settings.get({incognito: false},
350 this.handleRegularState_.bind(this));
351 if (this.isAllowedIncognitoAccess_) {
352 chrome.proxy.settings.get({incognito: true},
353 this.handleIncognitoState_.bind(this));
358 * Handles the response from 'proxy.settings.get' for regular
361 * @param {ProxyFormController.WrappedProxyConfig} c The proxy data and
362 * extension's level of control thereof.
365 handleRegularState_: function(c) {
366 if (c.levelOfControl === ProxyFormController.LevelOfControl.AVAILABLE ||
367 c.levelOfControl === ProxyFormController.LevelOfControl.CONTROLLING) {
368 this.recalcFormValues_(c.value);
369 this.config_.regular = c.value;
371 this.handleLackOfControl_(c.levelOfControl);
376 * Handles the response from 'proxy.settings.get' for incognito
379 * @param {ProxyFormController.WrappedProxyConfig} c The proxy data and
380 * extension's level of control thereof.
383 handleIncognitoState_: function(c) {
384 if (c.levelOfControl === ProxyFormController.LevelOfControl.AVAILABLE ||
385 c.levelOfControl === ProxyFormController.LevelOfControl.CONTROLLING) {
386 if (this.isIncognitoMode_())
387 this.recalcFormValues_(c.value);
389 this.config_.incognito = c.value;
391 this.handleLackOfControl_(c.levelOfControl);
396 * Binds event handlers for the various bits and pieces of the form that
397 * are interesting to the controller.
401 bindEventHandlers_: function() {
402 this.form_.addEventListener('click', this.dispatchFormClick_.bind(this));
407 * When a `click` event is triggered on the form, this function handles it by
408 * analyzing the context, and dispatching the click to the correct handler.
410 * @param {Event} e The event to be handled.
412 * @return {boolean} True if the event should bubble, false otherwise.
414 dispatchFormClick_: function(e) {
418 if (t.nodeName === 'INPUT' && t.getAttribute('type') === 'submit') {
419 return this.applyChanges_(e);
421 // Case 2: "Use the same proxy for all protocols" in an active section
422 } else if (t.nodeName === 'INPUT' &&
423 t.getAttribute('type') === 'checkbox' &&
424 t.parentNode.parentNode.parentNode.classList.contains('active')
426 return this.toggleSingleProxyConfig_(e);
428 // Case 3: "Flip to incognito mode."
429 } else if (t.nodeName === 'BUTTON') {
430 return this.toggleIncognitoMode_(e);
432 // Case 4: Click on something random: maybe changing active config group?
434 // Walk up the tree until we hit `form > fieldset` or fall off the top
435 while (t && (t.nodeName !== 'FIELDSET' ||
436 t.parentNode.nodeName !== 'FORM')) {
440 this.changeActive_(t);
449 * Sets the form's active config group.
451 * @param {DOMElement} fieldset The configuration group to activate.
454 changeActive_: function(fieldset) {
455 for (var i = 0; i < this.configGroups_.length; i++) {
456 var el = this.configGroups_[i];
457 var radio = el.querySelector("input[type='radio']");
458 if (el === fieldset) {
459 el.classList.add('active');
460 radio.checked = true;
462 el.classList.remove('active');
465 this.recalcDisabledInputs_();
470 * Recalculates the `disabled` state of the form's input elements, based
471 * on the currently active group, and that group's contents.
475 recalcDisabledInputs_: function() {
477 for (i = 0; i < this.configGroups_.length; i++) {
478 var el = this.configGroups_[i];
479 var inputs = el.querySelectorAll(
480 "input:not([type='radio']), select, textarea");
481 if (el.classList.contains('active')) {
482 for (j = 0; j < inputs.length; j++) {
483 inputs[j].removeAttribute('disabled');
486 for (j = 0; j < inputs.length; j++) {
487 inputs[j].setAttribute('disabled', 'disabled');
495 * Handler called in response to click on form's submission button. Generates
496 * the proxy configuration and passes it to `useCustomProxySettings`, or
497 * handles errors in user input.
499 * Proxy errors (and the browser action's badge) are cleared upon setting new
502 * @param {Event} e DOM event generated by the user's click.
505 applyChanges_: function(e) {
509 if (this.isIncognitoMode_())
510 this.config_.incognito = this.generateProxyConfig_();
512 this.config_.regular = this.generateProxyConfig_();
514 chrome.proxy.settings.set(
515 {value: this.config_.regular, scope: 'regular'},
516 this.callbackForRegularSettings_.bind(this));
517 chrome.extension.sendRequest({type: 'clearError'});
521 * Called in response to setting a regular window's proxy settings: checks
522 * for `lastError`, and then sets incognito settings (if they exist).
526 callbackForRegularSettings_: function() {
527 if (chrome.runtime.lastError) {
528 this.generateAlert_(chrome.i18n.getMessage('errorSettingRegularProxy'));
531 if (this.config_.incognito) {
532 chrome.proxy.settings.set(
533 {value: this.config_.incognito, scope: 'incognito_persistent'},
534 this.callbackForIncognitoSettings_.bind(this));
536 ProxyFormController.setPersistedSettings(this.config_);
537 this.generateAlert_(chrome.i18n.getMessage('successfullySetProxy'));
542 * Called in response to setting an incognito window's proxy settings: checks
543 * for `lastError` and sets a success message.
547 callbackForIncognitoSettings_: function() {
548 if (chrome.runtime.lastError) {
549 this.generateAlert_(chrome.i18n.getMessage('errorSettingIncognitoProxy'));
552 ProxyFormController.setPersistedSettings(this.config_);
554 chrome.i18n.getMessage('successfullySetProxy'));
558 * Generates an alert overlay inside the proxy's popup, then closes the popup
559 * after a short delay.
561 * @param {string} msg The message to be displayed in the overlay.
562 * @param {?boolean} close Should the window be closed? Defaults to true.
565 generateAlert_: function(msg, close) {
566 var success = document.createElement('div');
567 success.classList.add('overlay');
568 success.setAttribute('role', 'alert');
569 success.textContent = msg;
570 document.body.appendChild(success);
572 setTimeout(function() { success.classList.add('visible'); }, 10);
573 setTimeout(function() {
575 success.classList.remove('visible');
583 * Parses the proxy configuration form, and generates a ProxyConfig object
584 * that can be passed to `useCustomProxyConfig`.
586 * @see http://code.google.com/chrome/extensions/trunk/proxy.html
587 * @return {ProxyConfig} The proxy configuration represented by the form.
590 generateProxyConfig_: function() {
591 var active = document.getElementsByClassName('active')[0];
593 case ProxyFormController.ProxyTypes.SYSTEM:
594 return {mode: 'system'};
595 case ProxyFormController.ProxyTypes.DIRECT:
596 return {mode: 'direct'};
597 case ProxyFormController.ProxyTypes.PAC:
598 var pacScriptURL = this.pacURL;
599 var pacManual = this.manualPac;
601 return {mode: 'pac_script',
602 pacScript: {url: pacScriptURL, mandatory: true}};
604 return {mode: 'pac_script',
605 pacScript: {data: pacManual, mandatory: true}};
607 return {mode: 'auto_detect'};
608 case ProxyFormController.ProxyTypes.FIXED:
609 var config = {mode: 'fixed_servers'};
610 if (this.singleProxy) {
612 singleProxy: this.singleProxy,
613 bypassList: this.bypassList
617 proxyForHttp: this.httpProxy,
618 proxyForHttps: this.httpsProxy,
619 proxyForFtp: this.ftpProxy,
620 fallbackProxy: this.fallbackProxy,
621 bypassList: this.bypassList
630 * Sets the proper display classes based on the "Use the same proxy server
631 * for all protocols" checkbox. Expects to be called as an event handler
632 * when that field is clicked.
634 * @param {Event} e The `click` event to respond to.
637 toggleSingleProxyConfig_: function(e) {
638 var checkbox = e.target;
639 if (checkbox.nodeName === 'INPUT' &&
640 checkbox.getAttribute('type') === 'checkbox') {
641 if (checkbox.checked)
642 checkbox.parentNode.parentNode.classList.add('single');
644 checkbox.parentNode.parentNode.classList.remove('single');
650 * Returns the form's current incognito status.
652 * @return {boolean} True if the form is in incognito mode, false otherwise.
655 isIncognitoMode_: function(e) {
656 return this.form_.parentNode.classList.contains('incognito');
661 * Toggles the form's incognito mode. Saves the current state to an object
662 * property for later use, clears the form, and toggles the appropriate state.
664 * @param {Event} e The `click` event to respond to.
667 toggleIncognitoMode_: function(e) {
668 var div = this.form_.parentNode;
669 var button = document.getElementsByTagName('button')[0];
671 // Cancel the button click.
675 // If we can't access Incognito settings, throw a message and return.
676 if (!this.isAllowedIncognitoAccess_) {
677 var msg = "I'm sorry, Dave, I'm afraid I can't do that. Give me access " +
678 "to Incognito settings by checking the checkbox labeled " +
679 "'Allow in Incognito mode', which is visible at " +
680 "chrome://extensions.";
681 this.generateAlert_(msg, false);
685 if (this.isIncognitoMode_()) {
686 // In incognito mode, switching to cognito.
687 this.config_.incognito = this.generateProxyConfig_();
688 div.classList.remove('incognito');
689 this.recalcFormValues_(this.config_.regular);
690 button.innerText = 'Configure incognito window settings.';
692 // In cognito mode, switching to incognito.
693 this.config_.regular = this.generateProxyConfig_();
694 div.classList.add('incognito');
695 this.recalcFormValues_(this.config_.incognito);
696 button.innerText = 'Configure regular window settings.';
702 * Sets the form's values based on a ProxyConfig.
704 * @param {!ProxyConfig} c The ProxyConfig object.
707 recalcFormValues_: function(c) {
708 // Normalize `auto_detect`
709 if (c.mode === 'auto_detect')
710 c.mode = 'pac_script';
711 // Activate one of the groups, based on `mode`.
712 this.changeActive_(document.getElementById(c.mode));
713 // Populate the PAC script
716 this.pacURL = c.pacScript.url;
720 // Evaluate the `rules`
723 if (rules.singleProxy) {
724 this.singleProxy = rules.singleProxy;
726 this.singleProxy = null;
727 this.httpProxy = rules.proxyForHttp;
728 this.httpsProxy = rules.proxyForHttps;
729 this.ftpProxy = rules.proxyForFtp;
730 this.fallbackProxy = rules.fallbackProxy;
732 this.bypassList = rules.bypassList;
734 this.singleProxy = null;
735 this.httpProxy = null;
736 this.httpsProxy = null;
737 this.ftpProxy = null;
738 this.fallbackProxy = null;
739 this.bypassList = '';
745 * Handles the case in which this extension doesn't have the ability to
746 * control the Proxy settings, either because of an overriding policy
747 * or an extension with higher priority.
749 * @param {ProxyFormController.LevelOfControl} l The level of control this
750 * extension has over the proxy settings.
753 handleLackOfControl_: function(l) {
755 if (l === ProxyFormController.LevelOfControl.NO_ACCESS)
756 msg = chrome.i18n.getMessage('errorNoExtensionAccess');
757 else if (l === ProxyFormController.LevelOfControl.OTHER_EXTENSION)
758 msg = chrome.i18n.getMessage('errorOtherExtensionControls');
759 this.generateAlert_(msg);
764 * Handle the case in which errors have been generated outside the context
769 handleProxyErrors_: function() {
770 chrome.extension.sendRequest(
772 this.handleProxyErrorHandlerResponse_.bind(this));
776 * Handles response from ProxyErrorHandler
778 * @param {{result: !string}} response The message sent in response to this
781 handleProxyErrorHandlerResponse_: function(response) {
782 if (response.result !== null) {
783 var error = JSON.parse(response.result);
784 console.error(error);
785 // TODO(mkwst): Do something more interesting
787 chrome.i18n.getMessage(
788 error.details ? 'errorProxyDetailedError' : 'errorProxyError',
789 [error.error, error.details]),