- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / common / extensions / docs / examples / extensions / proxy_configuration / proxy_form_controller.js
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.
4
5 /**
6  * @fileoverview This file implements the ProxyFormController class, which
7  * wraps a form element with logic that enables implementation of proxy
8  * settings.
9  *
10  * @author mkwst@google.com (Mike West)
11  */
12
13 /**
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.
17  *
18  * @param {string} id The form's DOM ID.
19  * @constructor
20  */
21 var ProxyFormController = function(id) {
22   /**
23    * The wrapped form element
24    * @type {Node}
25    * @private
26    */
27   this.form_ = document.getElementById(id);
28
29   // Throw an error if the element either doesn't exist, or isn't a form.
30   if (!this.form_)
31     throw chrome.i18n.getMessage('errorIdNotFound', id);
32   else if (this.form_.nodeName !== 'FORM')
33     throw chrome.i18n.getMessage('errorIdNotForm', id);
34
35   /**
36    * Cached references to the `fieldset` groups that define the configuration
37    * options presented to the user.
38    *
39    * @type {NodeList}
40    * @private
41    */
42   this.configGroups_ = document.querySelectorAll('#' + id + ' > fieldset');
43
44   this.bindEventHandlers_();
45   this.readCurrentState_();
46
47   // Handle errors
48   this.handleProxyErrors_();
49 };
50
51 ///////////////////////////////////////////////////////////////////////////////
52
53 /**
54  * The proxy types we're capable of handling.
55  * @enum {string}
56  */
57 ProxyFormController.ProxyTypes = {
58   AUTO: 'auto_detect',
59   PAC: 'pac_script',
60   DIRECT: 'direct',
61   FIXED: 'fixed_servers',
62   SYSTEM: 'system'
63 };
64
65 /**
66  * The window types we're capable of handling.
67  * @enum {int}
68  */
69 ProxyFormController.WindowTypes = {
70   REGULAR: 1,
71   INCOGNITO: 2
72 };
73
74 /**
75  * The extension's level of control of Chrome's roxy setting
76  * @enum {string}
77  */
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'
83 };
84
85 /**
86  * The response type from 'proxy.settings.get'
87  *
88  * @typedef {{value: ProxyConfig,
89  *     levelOfControl: ProxyFormController.LevelOfControl}}
90  */
91 ProxyFormController.WrappedProxyConfig;
92
93 ///////////////////////////////////////////////////////////////////////////////
94
95 /**
96  * Retrieves proxy settings that have been persisted across restarts.
97  *
98  * @return {?ProxyConfig} The persisted proxy configuration, or null if no
99  *     value has been persisted.
100  * @static
101  */
102 ProxyFormController.getPersistedSettings = function() {
103   var result = null;
104   if (window.localStorage['proxyConfig'] !== undefined)
105     result = JSON.parse(window.localStorage['proxyConfig']);
106   return result ? result : null;
107 };
108
109
110 /**
111  * Persists proxy settings across restarts.
112  *
113  * @param {!ProxyConfig} config The proxy config to persist.
114  * @static
115  */
116 ProxyFormController.setPersistedSettings = function(config) {
117   window.localStorage['proxyConfig'] = JSON.stringify(config);
118 };
119
120 ///////////////////////////////////////////////////////////////////////////////
121
122 ProxyFormController.prototype = {
123   /**
124    * The form's current state.
125    * @type {regular: ?ProxyConfig, incognito: ?ProxyConfig}
126    * @private
127    */
128   config_: {regular: null, incognito: null},
129
130   /**
131    * Do we have access to incognito mode?
132    * @type {boolean}
133    * @private
134    */
135   isAllowedIncognitoAccess_: false,
136
137   /**
138    * @return {string} The PAC file URL (or an empty string).
139    */
140   get pacURL() {
141     return document.getElementById('autoconfigURL').value;
142   },
143
144
145   /**
146    * @param {!string} value The PAC file URL.
147    */
148   set pacURL(value) {
149     document.getElementById('autoconfigURL').value = value;
150   },
151
152
153   /**
154    * @return {string} The PAC file data (or an empty string).
155    */
156   get manualPac() {
157     return document.getElementById('autoconfigData').value;
158   },
159
160
161   /**
162    * @param {!string} value The PAC file data.
163    */
164   set manualPac(value) {
165     document.getElementById('autoconfigData').value = value;
166   },
167
168
169   /**
170    * @return {Array.<string>} A list of hostnames that should bypass the proxy.
171    */
172   get bypassList() {
173     return document.getElementById('bypassList').value.split(/\s*(?:,|^)\s*/m);
174   },
175
176
177   /**
178    * @param {?Array.<string>} data A list of hostnames that should bypass
179    *     the proxy. If empty, the bypass list is emptied.
180    */
181   set bypassList(data) {
182     if (!data)
183       data = [];
184     document.getElementById('bypassList').value = data.join(', ');
185   },
186
187
188   /**
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.
192    */
193   get singleProxy() {
194     var checkbox = document.getElementById('singleProxyForEverything');
195     return checkbox.checked ? this.httpProxy : null;
196   },
197
198
199   /**
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.
203    */
204   set singleProxy(data) {
205     var checkbox = document.getElementById('singleProxyForEverything');
206     checkbox.checked = !!data;
207
208     if (data)
209       this.httpProxy = data;
210
211     if (checkbox.checked)
212       checkbox.parentNode.parentNode.classList.add('single');
213     else
214       checkbox.parentNode.parentNode.classList.remove('single');
215   },
216
217   /**
218    * @return {?ProxyServer} An object containing the proxy server host, port
219    *     and scheme.
220    */
221   get httpProxy() {
222     return this.getProxyImpl_('Http');
223   },
224
225
226   /**
227    * @param {?ProxyServer} data An object containing the proxy server host,
228    *     port, and scheme. If empty, empties the proxy setting.
229    */
230   set httpProxy(data) {
231     this.setProxyImpl_('Http', data);
232   },
233
234
235   /**
236    * @return {?ProxyServer} An object containing the proxy server host, port
237    *     and scheme.
238    */
239   get httpsProxy() {
240     return this.getProxyImpl_('Https');
241   },
242
243
244   /**
245    * @param {?ProxyServer} data An object containing the proxy server host,
246    *     port, and scheme. If empty, empties the proxy setting.
247    */
248   set httpsProxy(data) {
249     this.setProxyImpl_('Https', data);
250   },
251
252
253   /**
254    * @return {?ProxyServer} An object containing the proxy server host, port
255    *     and scheme.
256    */
257   get ftpProxy() {
258     return this.getProxyImpl_('Ftp');
259   },
260
261
262   /**
263    * @param {?ProxyServer} data An object containing the proxy server host,
264    *     port, and scheme. If empty, empties the proxy setting.
265    */
266   set ftpProxy(data) {
267     this.setProxyImpl_('Ftp', data);
268   },
269
270
271   /**
272    * @return {?ProxyServer} An object containing the proxy server host, port
273    *     and scheme.
274    */
275   get fallbackProxy() {
276     return this.getProxyImpl_('Fallback');
277   },
278
279
280   /**
281    * @param {?ProxyServer} data An object containing the proxy server host,
282    *     port, and scheme. If empty, empties the proxy setting.
283    */
284   set fallbackProxy(data) {
285     this.setProxyImpl_('Fallback', data);
286   },
287
288
289   /**
290    * @param {string} type The type of proxy that's being set ("Http",
291    *     "Https", etc.).
292    * @return {?ProxyServer} An object containing the proxy server host,
293    *     port, and scheme.
294    * @private
295    */
296   getProxyImpl_: function(type) {
297     var result = {
298       scheme: document.getElementById('proxyScheme' + type).value,
299       host: document.getElementById('proxyHost' + type).value,
300       port: parseInt(document.getElementById('proxyPort' + type).value, 10)
301     };
302     return (result.scheme && result.host && result.port) ? result : undefined;
303   },
304
305
306   /**
307    * A generic mechanism for setting proxy data.
308    *
309    * @see http://code.google.com/chrome/extensions/trunk/proxy.html
310    * @param {string} type The type of proxy that's being set ("Http",
311    *     "Https", etc.).
312    * @param {?ProxyServer} data An object containing the proxy server host,
313    *     port, and scheme. If empty, empties the proxy setting.
314    * @private
315    */
316   setProxyImpl_: function(type, data) {
317     if (!data)
318       data = {scheme: 'http', host: '', port: ''};
319
320     document.getElementById('proxyScheme' + type).value = data.scheme;
321     document.getElementById('proxyHost' + type).value = data.host;
322     document.getElementById('proxyPort' + type).value = data.port;
323   },
324
325 ///////////////////////////////////////////////////////////////////////////////
326
327   /**
328    * Calls the proxy API to read the current settings, and populates the form
329    * accordingly.
330    *
331    * @private
332    */
333   readCurrentState_: function() {
334     chrome.extension.isAllowedIncognitoAccess(
335         this.handleIncognitoAccessResponse_.bind(this));
336   },
337
338   /**
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.
343    *
344    * @param {boolean} state The state of incognito access.
345    * @private
346    */
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));
354     }
355   },
356
357   /**
358    * Handles the response from 'proxy.settings.get' for regular
359    * settings.
360    *
361    * @param {ProxyFormController.WrappedProxyConfig} c The proxy data and
362    *     extension's level of control thereof.
363    * @private
364    */
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;
370     } else {
371       this.handleLackOfControl_(c.levelOfControl);
372     }
373   },
374
375   /**
376    * Handles the response from 'proxy.settings.get' for incognito
377    * settings.
378    *
379    * @param {ProxyFormController.WrappedProxyConfig} c The proxy data and
380    *     extension's level of control thereof.
381    * @private
382    */
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);
388
389       this.config_.incognito = c.value;
390     } else {
391       this.handleLackOfControl_(c.levelOfControl);
392     }
393   },
394
395   /**
396    * Binds event handlers for the various bits and pieces of the form that
397    * are interesting to the controller.
398    *
399    * @private
400    */
401   bindEventHandlers_: function() {
402     this.form_.addEventListener('click', this.dispatchFormClick_.bind(this));
403   },
404
405
406   /**
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.
409    *
410    * @param {Event} e The event to be handled.
411    * @private
412    * @return {boolean} True if the event should bubble, false otherwise.
413    */
414   dispatchFormClick_: function(e) {
415     var t = e.target;
416
417     // Case 1: "Apply"
418     if (t.nodeName === 'INPUT' && t.getAttribute('type') === 'submit') {
419       return this.applyChanges_(e);
420
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')
425               ) {
426       return this.toggleSingleProxyConfig_(e);
427
428     // Case 3: "Flip to incognito mode."
429     } else if (t.nodeName === 'BUTTON') {
430       return this.toggleIncognitoMode_(e);
431
432     // Case 4: Click on something random: maybe changing active config group?
433     } else {
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')) {
437         t = t.parentNode;
438       }
439       if (t) {
440         this.changeActive_(t);
441         return false;
442       }
443     }
444     return true;
445   },
446
447
448   /**
449    * Sets the form's active config group.
450    *
451    * @param {DOMElement} fieldset The configuration group to activate.
452    * @private
453    */
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;
461       } else {
462         el.classList.remove('active');
463       }
464     }
465     this.recalcDisabledInputs_();
466   },
467
468
469   /**
470    * Recalculates the `disabled` state of the form's input elements, based
471    * on the currently active group, and that group's contents.
472    *
473    * @private
474    */
475   recalcDisabledInputs_: function() {
476     var i, j;
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');
484         }
485       } else {
486         for (j = 0; j < inputs.length; j++) {
487           inputs[j].setAttribute('disabled', 'disabled');
488         }
489       }
490     }
491   },
492
493
494   /**
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.
498    *
499    * Proxy errors (and the browser action's badge) are cleared upon setting new
500    * values.
501    *
502    * @param {Event} e DOM event generated by the user's click.
503    * @private
504    */
505   applyChanges_: function(e) {
506     e.preventDefault();
507     e.stopPropagation();
508
509     if (this.isIncognitoMode_())
510       this.config_.incognito = this.generateProxyConfig_();
511     else
512       this.config_.regular = this.generateProxyConfig_();
513
514     chrome.proxy.settings.set(
515         {value: this.config_.regular, scope: 'regular'},
516         this.callbackForRegularSettings_.bind(this));
517     chrome.extension.sendRequest({type: 'clearError'});
518   },
519
520   /**
521    * Called in response to setting a regular window's proxy settings: checks
522    * for `lastError`, and then sets incognito settings (if they exist).
523    *
524    * @private
525    */
526   callbackForRegularSettings_: function() {
527     if (chrome.runtime.lastError) {
528       this.generateAlert_(chrome.i18n.getMessage('errorSettingRegularProxy'));
529       return;
530     }
531     if (this.config_.incognito) {
532       chrome.proxy.settings.set(
533           {value: this.config_.incognito, scope: 'incognito_persistent'},
534           this.callbackForIncognitoSettings_.bind(this));
535     } else {
536       ProxyFormController.setPersistedSettings(this.config_);
537       this.generateAlert_(chrome.i18n.getMessage('successfullySetProxy'));
538     }
539   },
540
541   /**
542    * Called in response to setting an incognito window's proxy settings: checks
543    * for `lastError` and sets a success message.
544    *
545    * @private
546    */
547   callbackForIncognitoSettings_: function() {
548     if (chrome.runtime.lastError) {
549       this.generateAlert_(chrome.i18n.getMessage('errorSettingIncognitoProxy'));
550       return;
551     }
552     ProxyFormController.setPersistedSettings(this.config_);
553     this.generateAlert_(
554         chrome.i18n.getMessage('successfullySetProxy'));
555   },
556
557   /**
558    * Generates an alert overlay inside the proxy's popup, then closes the popup
559    * after a short delay.
560    *
561    * @param {string} msg The message to be displayed in the overlay.
562    * @param {?boolean} close Should the window be closed?  Defaults to true.
563    * @private
564    */
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);
571
572     setTimeout(function() { success.classList.add('visible'); }, 10);
573     setTimeout(function() {
574       if (close === false)
575         success.classList.remove('visible');
576       else
577         window.close();
578     }, 4000);
579   },
580
581
582   /**
583    * Parses the proxy configuration form, and generates a ProxyConfig object
584    * that can be passed to `useCustomProxyConfig`.
585    *
586    * @see http://code.google.com/chrome/extensions/trunk/proxy.html
587    * @return {ProxyConfig} The proxy configuration represented by the form.
588    * @private
589    */
590   generateProxyConfig_: function() {
591     var active = document.getElementsByClassName('active')[0];
592     switch (active.id) {
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;
600         if (pacScriptURL)
601           return {mode: 'pac_script',
602                   pacScript: {url: pacScriptURL, mandatory: true}};
603         else if (pacManual)
604           return {mode: 'pac_script',
605                   pacScript: {data: pacManual, mandatory: true}};
606         else
607           return {mode: 'auto_detect'};
608       case ProxyFormController.ProxyTypes.FIXED:
609         var config = {mode: 'fixed_servers'};
610         if (this.singleProxy) {
611           config.rules = {
612             singleProxy: this.singleProxy,
613             bypassList: this.bypassList
614           };
615         } else {
616           config.rules = {
617             proxyForHttp: this.httpProxy,
618             proxyForHttps: this.httpsProxy,
619             proxyForFtp: this.ftpProxy,
620             fallbackProxy: this.fallbackProxy,
621             bypassList: this.bypassList
622           };
623         }
624         return config;
625     }
626   },
627
628
629   /**
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.
633    *
634    * @param {Event} e The `click` event to respond to.
635    * @private
636    */
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');
643       else
644         checkbox.parentNode.parentNode.classList.remove('single');
645     }
646   },
647
648
649   /**
650    * Returns the form's current incognito status.
651    *
652    * @return {boolean} True if the form is in incognito mode, false otherwise.
653    * @private
654    */
655   isIncognitoMode_: function(e) {
656     return this.form_.parentNode.classList.contains('incognito');
657   },
658
659
660   /**
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.
663    *
664    * @param {Event} e The `click` event to respond to.
665    * @private
666    */
667   toggleIncognitoMode_: function(e) {
668     var div = this.form_.parentNode;
669     var button = document.getElementsByTagName('button')[0];
670
671     // Cancel the button click.
672     e.preventDefault();
673     e.stopPropagation();
674
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);
682       return;
683     }
684
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.';
691     } else {
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.';
697     }
698   },
699
700
701   /**
702    * Sets the form's values based on a ProxyConfig.
703    *
704    * @param {!ProxyConfig} c The ProxyConfig object.
705    * @private
706    */
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
714     if (c.pacScript) {
715       if (c.pacScript.url)
716         this.pacURL = c.pacScript.url;
717     } else {
718       this.pacURL = '';
719     }
720     // Evaluate the `rules`
721     if (c.rules) {
722       var rules = c.rules;
723       if (rules.singleProxy) {
724         this.singleProxy = rules.singleProxy;
725       } else {
726         this.singleProxy = null;
727         this.httpProxy = rules.proxyForHttp;
728         this.httpsProxy = rules.proxyForHttps;
729         this.ftpProxy = rules.proxyForFtp;
730         this.fallbackProxy = rules.fallbackProxy;
731       }
732       this.bypassList = rules.bypassList;
733     } else {
734       this.singleProxy = null;
735       this.httpProxy = null;
736       this.httpsProxy = null;
737       this.ftpProxy = null;
738       this.fallbackProxy = null;
739       this.bypassList = '';
740     }
741   },
742
743
744   /**
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.
748    *
749    * @param {ProxyFormController.LevelOfControl} l The level of control this
750    *     extension has over the proxy settings.
751    * @private
752    */
753   handleLackOfControl_: function(l) {
754     var msg;
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);
760   },
761
762
763   /**
764    * Handle the case in which errors have been generated outside the context
765    * of this popup.
766    *
767    * @private
768    */
769   handleProxyErrors_: function() {
770     chrome.extension.sendRequest(
771         {type: 'getError'},
772         this.handleProxyErrorHandlerResponse_.bind(this));
773   },
774
775   /**
776    * Handles response from ProxyErrorHandler
777    *
778    * @param {{result: !string}} response The message sent in response to this
779    *     popup's request.
780    */
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
786       this.generateAlert_(
787           chrome.i18n.getMessage(
788               error.details ? 'errorProxyDetailedError' : 'errorProxyError',
789               [error.error, error.details]),
790           false);
791     }
792   }
793 };