Only grant permissions to new extensions from sync if they have the expected version
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / cvox2 / background / background.js
blob193810f0e44ba4b1dd9aa394f6e0d4bcf40ecc79
1 // Copyright 2014 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.
5 /**
6  * @fileoverview The entry point for all ChromeVox2 related code for the
7  * background page.
8  */
10 goog.provide('Background');
11 goog.provide('global');
13 goog.require('AutomationPredicate');
14 goog.require('AutomationUtil');
15 goog.require('ClassicCompatibility');
16 goog.require('Output');
17 goog.require('Output.EventType');
18 goog.require('cursors.Cursor');
19 goog.require('cvox.BrailleKeyCommand');
20 goog.require('cvox.ChromeVoxEditableTextBase');
21 goog.require('cvox.ExtensionBridge');
22 goog.require('cvox.NavBraille');
24 goog.scope(function() {
25 var AutomationNode = chrome.automation.AutomationNode;
26 var Dir = AutomationUtil.Dir;
27 var EventType = chrome.automation.EventType;
28 var RoleType = chrome.automation.RoleType;
30 /**
31  * All possible modes ChromeVox can run.
32  * @enum {string}
33  */
34 var ChromeVoxMode = {
35   CLASSIC: 'classic',
36   COMPAT: 'compat',
37   NEXT: 'next',
38   FORCE_NEXT: 'force_next'
41 /**
42  * ChromeVox2 background page.
43  * @constructor
44  */
45 Background = function() {
46   /**
47    * A list of site substring patterns to use with ChromeVox next. Keep these
48    * strings relatively specific.
49    * @type {!Array<string>}
50    * @private
51    */
52   this.whitelist_ = ['chromevox_next_test'];
54   /**
55    * @type {cursors.Range}
56    * @private
57    */
58   this.currentRange_ = null;
60   /**
61    * Which variant of ChromeVox is active.
62    * @type {ChromeVoxMode}
63    * @private
64    */
65   this.mode_ = ChromeVoxMode.CLASSIC;
67   /** @type {!ClassicCompatibility} @private */
68   this.compat_ = new ClassicCompatibility();
70   // Manually bind all functions to |this|.
71   for (var func in this) {
72     if (typeof(this[func]) == 'function')
73       this[func] = this[func].bind(this);
74   }
76   /**
77    * Maps an automation event to its listener.
78    * @type {!Object<EventType, function(Object) : void>}
79    */
80   this.listeners_ = {
81     alert: this.onEventDefault,
82     focus: this.onFocus,
83     hover: this.onEventDefault,
84     loadComplete: this.onLoadComplete,
85     menuStart: this.onEventDefault,
86     menuEnd: this.onEventDefault,
87     textChanged: this.onTextOrTextSelectionChanged,
88     textSelectionChanged: this.onTextOrTextSelectionChanged,
89     valueChanged: this.onValueChanged
90   };
92   chrome.automation.getDesktop(this.onGotDesktop);
94   // Handle messages directed to the Next background page.
95   cvox.ExtensionBridge.addMessageListener(function(msg, port) {
96     var target = msg['target'];
97     var action = msg['action'];
99     switch (target) {
100       case 'next':
101         if (action == 'getIsClassicEnabled') {
102           var url = msg['url'];
103           var isClassicEnabled = this.shouldEnableClassicForUrl_(url);
104           port.postMessage({
105             target: 'next',
106             isClassicEnabled: isClassicEnabled
107           });
108         }
109         break;
110     }
111   }.bind(this));
114 Background.prototype = {
115   /** Forces ChromeVox Next to be active for all tabs. */
116   forceChromeVoxNextActive: function() {
117     this.setChromeVoxMode(ChromeVoxMode.FORCE_NEXT);
118   },
120   /**
121    * Handles all setup once a new automation tree appears.
122    * @param {chrome.automation.AutomationNode} desktop
123    */
124   onGotDesktop: function(desktop) {
125     // Register all automation event listeners.
126     for (var eventType in this.listeners_)
127       desktop.addEventListener(eventType, this.listeners_[eventType], true);
129     // Register a tree change observer.
130     chrome.automation.addTreeChangeObserver(this.onTreeChange);
132     // The focused state gets set on the containing webView node.
133     var webView = desktop.find({role: RoleType.webView,
134                                 state: {focused: true}});
135     if (webView) {
136       var root = webView.find({role: RoleType.rootWebArea});
137       if (root) {
138         this.onLoadComplete(
139             {target: root,
140              type: chrome.automation.EventType.loadComplete});
141       }
142     }
143   },
145   /**
146    * Handles chrome.commands.onCommand.
147    * @param {string} command
148    * @param {boolean=} opt_skipCompat Whether to skip compatibility checks.
149    */
150   onGotCommand: function(command, opt_skipCompat) {
151     if (!this.currentRange_)
152       return;
154     if (!opt_skipCompat && this.mode_ === ChromeVoxMode.COMPAT) {
155       if (this.compat_.onGotCommand(command))
156         return;
157     }
159     var current = this.currentRange_;
160     var dir = Dir.FORWARD;
161     var pred = null;
162     var predErrorMsg = undefined;
163     switch (command) {
164       case 'nextCharacter':
165         current = current.move(cursors.Unit.CHARACTER, Dir.FORWARD);
166         break;
167       case 'previousCharacter':
168         current = current.move(cursors.Unit.CHARACTER, Dir.BACKWARD);
169         break;
170       case 'nextWord':
171         current = current.move(cursors.Unit.WORD, Dir.FORWARD);
172         break;
173       case 'previousWord':
174         current = current.move(cursors.Unit.WORD, Dir.BACKWARD);
175         break;
176       case 'nextLine':
177         current = current.move(cursors.Unit.LINE, Dir.FORWARD);
178         break;
179       case 'previousLine':
180         current = current.move(cursors.Unit.LINE, Dir.BACKWARD);
181         break;
182       case 'nextButton':
183         dir = Dir.FORWARD;
184         pred = AutomationPredicate.button;
185         predErrorMsg = 'no_next_button';
186         break;
187       case 'previousButton':
188         dir = Dir.BACKWARD;
189         pred = AutomationPredicate.button;
190         predErrorMsg = 'no_previous_button';
191         break;
192       case 'nextCheckBox':
193         dir = Dir.FORWARD;
194         pred = AutomationPredicate.checkBox;
195         predErrorMsg = 'no_next_checkbox';
196         break;
197       case 'previousCheckBox':
198         dir = Dir.BACKWARD;
199         pred = AutomationPredicate.checkBox;
200         predErrorMsg = 'no_previous_checkbox';
201         break;
202       case 'nextComboBox':
203         dir = Dir.FORWARD;
204         pred = AutomationPredicate.comboBox;
205         predErrorMsg = 'no_next_combo_box';
206         break;
207       case 'previousComboBox':
208         dir = Dir.BACKWARD;
209         pred = AutomationPredicate.comboBox;
210         predErrorMsg = 'no_previous_combo_box';
211         break;
212       case 'nextEditText':
213         dir = Dir.FORWARD;
214         pred = AutomationPredicate.editText;
215         predErrorMsg = 'no_next_edit_text';
216         break;
217       case 'previousEditText':
218         dir = Dir.BACKWARD;
219         pred = AutomationPredicate.editText;
220         predErrorMsg = 'no_previous_edit_text';
221         break;
222       case 'nextFormField':
223         dir = Dir.FORWARD;
224         pred = AutomationPredicate.formField;
225         predErrorMsg = 'no_next_form_field';
226         break;
227       case 'previousFormField':
228         dir = Dir.BACKWARD;
229         pred = AutomationPredicate.formField;
230         predErrorMsg = 'no_previous_form_field';
231         break;
232       case 'nextHeading':
233         dir = Dir.FORWARD;
234         pred = AutomationPredicate.heading;
235         predErrorMsg = 'no_next_heading';
236         break;
237       case 'previousHeading':
238         dir = Dir.BACKWARD;
239         pred = AutomationPredicate.heading;
240         predErrorMsg = 'no_previous_heading';
241         break;
242       case 'nextLink':
243         dir = Dir.FORWARD;
244         pred = AutomationPredicate.link;
245         predErrorMsg = 'no_next_link';
246         break;
247       case 'previousLink':
248         dir = Dir.BACKWARD;
249         pred = AutomationPredicate.link;
250         predErrorMsg = 'no_previous_link';
251         break;
252       case 'nextTable':
253         dir = Dir.FORWARD;
254         pred = AutomationPredicate.table;
255         predErrorMsg = 'no_next_table';
256         break;
257       case 'previousTable':
258         dir = Dir.BACKWARD;
259         pred = AutomationPredicate.table;
260         predErrorMsg = 'no_previous_table';
261         break;
262       case 'nextVisitedLink':
263         dir = Dir.FORWARD;
264         pred = AutomationPredicate.visitedLink;
265         predErrorMsg = 'no_next_visited_link';
266         break;
267       case 'previousVisitedLink':
268         dir = Dir.BACKWARD;
269         pred = AutomationPredicate.visitedLink;
270         predErrorMsg = 'no_previous_visited_link';
271         break;
272       case 'nextElement':
273         current = current.move(cursors.Unit.NODE, Dir.FORWARD);
274         break;
275       case 'previousElement':
276         current = current.move(cursors.Unit.NODE, Dir.BACKWARD);
277         break;
278       case 'goToBeginning':
279         var node =
280             AutomationUtil.findNodePost(current.start.node.root,
281                                         Dir.FORWARD,
282                                         AutomationPredicate.leaf);
283         if (node)
284           current = cursors.Range.fromNode(node);
285         break;
286       case 'goToEnd':
287         var node =
288             AutomationUtil.findNodePost(current.start.node.root,
289                                         Dir.BACKWARD,
290                                         AutomationPredicate.leaf);
291         if (node)
292           current = cursors.Range.fromNode(node);
293         break;
294       case 'doDefault':
295         if (this.currentRange_) {
296           var actionNode = this.currentRange_.start.node;
297           if (actionNode.role == RoleType.inlineTextBox)
298             actionNode = actionNode.parent;
299           actionNode.doDefault();
300         }
301         // Skip all other processing; if focus changes, we should get an event
302         // for that.
303         return;
304       case 'continuousRead':
305         global.isReadingContinuously = true;
306         var continueReading = function(prevRange) {
307           if (!global.isReadingContinuously || !this.currentRange_)
308             return;
310           new Output().withSpeechAndBraille(
311                   this.currentRange_, prevRange, Output.EventType.NAVIGATE)
312               .onSpeechEnd(function() { continueReading(prevRange); })
313               .go();
314           prevRange = this.currentRange_;
315           this.currentRange_ =
316               this.currentRange_.move(cursors.Unit.NODE, Dir.FORWARD);
318           if (!this.currentRange_ || this.currentRange_.equals(prevRange))
319             global.isReadingContinuously = false;
320         }.bind(this);
322         continueReading(null);
323         return;
324       case 'showContextMenu':
325         if (this.currentRange_) {
326           var actionNode = this.currentRange_.start.node;
327           if (actionNode.role == RoleType.inlineTextBox)
328             actionNode = actionNode.parent;
329           actionNode.showContextMenu();
330           return;
331         }
332         break;
333       case 'showOptionsPage':
334         var optionsPage = {url: 'chromevox/background/options.html'};
335         chrome.tabs.create(optionsPage);
336         break;
337     }
339     if (pred) {
340       var node = AutomationUtil.findNextNode(
341           current.getBound(dir).node, dir, pred);
343       if (node) {
344         current = cursors.Range.fromNode(node);
345       } else {
346         if (predErrorMsg) {
347           cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg(predErrorMsg),
348                                    cvox.QueueMode.FLUSH);
349         }
350         return;
351       }
352     }
354     if (current) {
355       // TODO(dtseng): Figure out what it means to focus a range.
356       var actionNode = current.start.node;
357       if (actionNode.role == RoleType.inlineTextBox)
358         actionNode = actionNode.parent;
359       actionNode.focus();
361       var prevRange = this.currentRange_;
362       this.currentRange_ = current;
364       new Output().withSpeechAndBraille(
365               this.currentRange_, prevRange, Output.EventType.NAVIGATE)
366           .go();
367     }
368   },
370   /**
371    * Handles a braille command.
372    * @param {!cvox.BrailleKeyEvent} evt
373    * @param {!cvox.NavBraille} content
374    * @return {boolean} True if evt was processed.
375    */
376   onBrailleKeyEvent: function(evt, content) {
377     if (this.mode_ === ChromeVoxMode.CLASSIC)
378       return false;
380     switch (evt.command) {
381       case cvox.BrailleKeyCommand.PAN_LEFT:
382         this.onGotCommand('previousElement', true);
383         break;
384       case cvox.BrailleKeyCommand.PAN_RIGHT:
385         this.onGotCommand('nextElement', true);
386         break;
387       case cvox.BrailleKeyCommand.LINE_UP:
388         this.onGotCommand('previousLine', true);
389         break;
390       case cvox.BrailleKeyCommand.LINE_DOWN:
391         this.onGotCommand('nextLine', true);
392         break;
393       case cvox.BrailleKeyCommand.TOP:
394         this.onGotCommand('goToBeginning', true);
395         break;
396       case cvox.BrailleKeyCommand.BOTTOM:
397         this.onGotCommand('goToEnd', true);
398         break;
399       case cvox.BrailleKeyCommand.ROUTING:
400         this.brailleRoutingCommand_(
401             content.text,
402             // Cast ok since displayPosition is always defined in this case.
403             /** @type {number} */ (evt.displayPosition));
404         break;
405       default:
406         return false;
407     }
408     return true;
409   },
411   /**
412    * Provides all feedback once ChromeVox's focus changes.
413    * @param {Object} evt
414    */
415   onEventDefault: function(evt) {
416     var node = evt.target;
418     if (!node)
419       return;
421     var prevRange = this.currentRange_;
423     this.currentRange_ = cursors.Range.fromNode(node);
425     // Check to see if we've crossed roots. Continue if we've crossed roots or
426     // are not within web content.
427     if (node.root.role == 'desktop' ||
428         !prevRange ||
429         prevRange.start.node.root != node.root)
430       this.setupChromeVoxVariants_(node.root.docUrl || '');
432     // Don't process nodes inside of web content if ChromeVox Next is inactive.
433     if (node.root.role != RoleType.desktop &&
434         this.mode_ === ChromeVoxMode.CLASSIC) {
435       chrome.accessibilityPrivate.setFocusRing([]);
436       return;
437     }
439     // Don't output if focused node hasn't changed.
440     if (prevRange &&
441         evt.type == 'focus' &&
442         this.currentRange_.equals(prevRange))
443       return;
445     new Output().withSpeechAndBraille(
446             this.currentRange_, prevRange, evt.type)
447         .go();
448   },
450   /**
451    * Provides all feedback once a focus event fires.
452    * @param {Object} evt
453    */
454   onFocus: function(evt) {
455     var node = evt.target;
457     // It almost never makes sense to place focus directly on a rootWebArea.
458     if (node.role == RoleType.rootWebArea) {
459       // Try to find a focusable descendant.
460       node = AutomationUtil.findNodePost(node,
461                                          Dir.FORWARD,
462                                          AutomationPredicate.focused) || node;
464       // Fall back to the first leaf node in the document.
465       if (node.role == RoleType.rootWebArea) {
466         node = AutomationUtil.findNodePost(node,
467                                            Dir.FORWARD,
468                                            AutomationPredicate.leaf);
469       }
470     }
471     this.onEventDefault({target: node, type: 'focus'});
472   },
474   /**
475    * Provides all feedback once a load complete event fires.
476    * @param {Object} evt
477    */
478   onLoadComplete: function(evt) {
479     this.setupChromeVoxVariants_(evt.target.docUrl);
481     // Don't process nodes inside of web content if ChromeVox Next is inactive.
482     if (evt.target.root.role != RoleType.desktop &&
483         this.mode_ === ChromeVoxMode.CLASSIC)
484       return;
486     // If initial focus was already placed on this page (e.g. if a user starts
487     // tabbing before load complete), then don't move ChromeVox's position on
488     // the page.
489     if (this.currentRange_ &&
490         this.currentRange_.start.node.role != RoleType.rootWebArea &&
491         this.currentRange_.start.node.root.docUrl == evt.target.docUrl)
492       return;
494     var root = evt.target;
495     var webView = root;
496     while (webView && webView.role != RoleType.webView)
497       webView = webView.parent;
499     if (!webView || !webView.state.focused)
500       return;
502     var node = AutomationUtil.findNodePost(root,
503         Dir.FORWARD,
504         AutomationPredicate.leaf);
506     if (node)
507       this.currentRange_ = cursors.Range.fromNode(node);
509     if (this.currentRange_)
510       new Output().withSpeechAndBraille(
511               this.currentRange_, null, evt.type)
512           .go();
513   },
515   /**
516    * Provides all feedback once a text selection change event fires.
517    * @param {Object} evt
518    */
519   onTextOrTextSelectionChanged: function(evt) {
520     // Don't process nodes inside of web content if ChromeVox Next is inactive.
521     if (evt.target.root.role != RoleType.desktop &&
522         this.mode_ === ChromeVoxMode.CLASSIC)
523       return;
525     if (!evt.target.state.focused)
526       return;
528     if (!this.currentRange_) {
529       this.onEventDefault(evt);
530       this.currentRange_ = cursors.Range.fromNode(evt.target);
531     }
533     var textChangeEvent = new cvox.TextChangeEvent(
534         evt.target.value,
535         evt.target.textSelStart,
536         evt.target.textSelEnd,
537         true);  // triggered by user
538     if (!this.editableTextHandler ||
539         evt.target != this.currentRange_.start.node) {
540       this.editableTextHandler =
541           new cvox.ChromeVoxEditableTextBase(
542               textChangeEvent.value,
543               textChangeEvent.start,
544               textChangeEvent.end,
545               evt.target.state['protected'],
546               cvox.ChromeVox.tts);
547     }
549     this.editableTextHandler.changed(textChangeEvent);
550     new Output().withBraille(
551             this.currentRange_, null, evt.type)
552         .go();
553   },
555   /**
556    * Provides all feedback once a value changed event fires.
557    * @param {Object} evt
558    */
559   onValueChanged: function(evt) {
560     // Don't process nodes inside of web content if ChromeVox Next is inactive.
561     if (evt.target.root.role != RoleType.desktop &&
562         this.mode_ === ChromeVoxMode.CLASSIC)
563       return;
565     if (!evt.target.state.focused)
566       return;
568     // Value change events fire on web text fields and text areas when pressing
569     // enter; suppress them.
570     if (!this.currentRange_ ||
571         evt.target.role != RoleType.textField) {
572       this.onEventDefault(evt);
573       this.currentRange_ = cursors.Range.fromNode(evt.target);
574     }
575   },
577   /**
578    * Called when the automation tree is changed.
579    * @param {chrome.automation.TreeChange} treeChange
580    */
581   onTreeChange: function(treeChange) {
582     var node = treeChange.target;
583     if (!node.containerLiveStatus)
584       return;
586     if (node.containerLiveRelevant.indexOf('additions') >= 0 &&
587         treeChange.type == 'nodeCreated')
588       this.outputLiveRegionChange_(node, null);
589     if (node.containerLiveRelevant.indexOf('text') >= 0 &&
590         treeChange.type == 'nodeChanged')
591       this.outputLiveRegionChange_(node, null);
592     if (node.containerLiveRelevant.indexOf('removals') >= 0 &&
593         treeChange.type == 'nodeRemoved')
594       this.outputLiveRegionChange_(node, '@live_regions_removed');
595   },
597   /**
598    * Given a node that needs to be spoken as part of a live region
599    * change and an additional optional format string, output the
600    * live region description.
601    * @param {!chrome.automation.AutomationNode} node The changed node.
602    * @param {?string} opt_prependFormatStr If set, a format string for
603    *     cvox2.Output to prepend to the output.
604    * @private
605    */
606   outputLiveRegionChange_: function(node, opt_prependFormatStr) {
607     var range = cursors.Range.fromNode(node);
608     var output = new Output();
609     if (opt_prependFormatStr) {
610       output.format(opt_prependFormatStr);
611     }
612     output.withSpeech(range, null, Output.EventType.NAVIGATE);
613     output.go();
614   },
616   /**
617    * Returns true if the url should have Classic running.
618    * @return {boolean}
619    * @private
620    */
621   shouldEnableClassicForUrl_: function(url) {
622     return this.mode_ != ChromeVoxMode.FORCE_NEXT &&
623         !this.isWhitelistedForCompat_(url) &&
624         !this.isWhitelistedForNext_(url);
625   },
627   /**
628    * @return {boolean}
629    * @private
630    */
631   isWhitelistedForCompat_: function(url) {
632     return url.indexOf('chrome://md-settings') != -1 ||
633           url.indexOf('chrome://oobe/login') != -1 ||
634           url.indexOf(
635               'https://accounts.google.com/embedded/setup/chromeos') === 0 ||
636           url === '';
637   },
639   /**
640    * @private
641    * @param {string} url
642    * @return {boolean} Whether the given |url| is whitelisted.
643    */
644   isWhitelistedForNext_: function(url) {
645     return this.whitelist_.some(function(item) {
646       return url.indexOf(item) != -1;
647     }.bind(this));
648   },
650   /**
651    * Setup ChromeVox variants.
652    * @param {string} url
653    * @private
654    */
655   setupChromeVoxVariants_: function(url) {
656     var mode = this.mode_;
657     if (mode != ChromeVoxMode.FORCE_NEXT) {
658       if (this.isWhitelistedForCompat_(url))
659         mode = ChromeVoxMode.COMPAT;
660       else if (this.isWhitelistedForNext_(url))
661         mode = ChromeVoxMode.NEXT;
662       else
663         mode = ChromeVoxMode.CLASSIC;
664     }
666     this.setChromeVoxMode(mode);
667   },
669   /**
670    * Disables classic ChromeVox in current web content.
671    */
672   disableClassicChromeVox_: function() {
673     cvox.ExtensionBridge.send({
674         message: 'SYSTEM_COMMAND',
675         command: 'killChromeVox'
676     });
677   },
679   /**
680    * Sets the current ChromeVox mode.
681    * @param {ChromeVoxMode} mode
682    */
683   setChromeVoxMode: function(mode) {
684     if (mode === ChromeVoxMode.NEXT ||
685         mode === ChromeVoxMode.COMPAT ||
686         mode === ChromeVoxMode.FORCE_NEXT) {
687       if (!chrome.commands.onCommand.hasListener(this.onGotCommand))
688         chrome.commands.onCommand.addListener(this.onGotCommand);
689     } else {
690       if (chrome.commands.onCommand.hasListener(this.onGotCommand))
691         chrome.commands.onCommand.removeListener(this.onGotCommand);
692     }
694     chrome.tabs.query({active: true}, function(tabs) {
695       if (mode === ChromeVoxMode.CLASSIC) {
696         // This case should do nothing because Classic gets injected by the
697         // extension system via our manifest. Once ChromeVox Next is enabled
698         // for tabs, re-enable.
699         // cvox.ChromeVox.injectChromeVoxIntoTabs(tabs);
700       } else {
701         // When in compat mode, if the focus is within the desktop tree proper,
702         // then do not disable content scripts.
703         if (this.currentRange_.start.node.root.role == 'desktop')
704           return;
706         this.disableClassicChromeVox_();
707       }
708     }.bind(this));
710     this.mode_ = mode;
711   },
713   /**
714    * @param {!cvox.Spannable} text
715    * @param {number} position
716    * @private
717    */
718   brailleRoutingCommand_: function(text, position) {
719     var actionNode = null;
720     var selectionSpan = null;
721     text.getSpans(position).forEach(function(span) {
722       if (span instanceof Output.SelectionSpan) {
723         selectionSpan = span;
724       } else if (span instanceof Output.NodeSpan) {
725         if (!actionNode ||
726             (text.getSpanEnd(actionNode) - text.getSpanStart(actionNode) >
727             text.getSpanEnd(span) - text.getSpanStart(span))) {
728           actionNode = span.node;
729         }
730       }
731     });
732     if (!actionNode)
733       return;
734     if (actionNode.role === RoleType.inlineTextBox)
735       actionNode = actionNode.parent;
736     actionNode.doDefault();
737     if (selectionSpan) {
738       var start = text.getSpanStart(selectionSpan);
739       actionNode.setSelection(position - start, position - start);
740     }
741   }
744 /** @type {Background} */
745 global.backgroundObj = new Background();
747 });  // goog.scope