1 // Copyright 2015 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 "content/browser/accessibility/one_shot_accessibility_tree_search.h"
6 
7 #include <stdint.h>
8 
9 #include "base/i18n/case_conversion.h"
10 #include "base/strings/string16.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "content/browser/accessibility/browser_accessibility.h"
13 #include "content/browser/accessibility/browser_accessibility_manager.h"
14 #include "ui/accessibility/ax_enums.mojom.h"
15 #include "ui/accessibility/ax_node.h"
16 #include "ui/accessibility/ax_role_properties.h"
17 
18 namespace content {
19 
20 // Given a node, populate a vector with all of the strings from that node's
21 // attributes that might be relevant for a text search.
GetNodeStrings(BrowserAccessibility * node,std::vector<base::string16> * strings)22 void GetNodeStrings(BrowserAccessibility* node,
23                     std::vector<base::string16>* strings) {
24   if (node->HasStringAttribute(ax::mojom::StringAttribute::kName))
25     strings->push_back(
26         node->GetString16Attribute(ax::mojom::StringAttribute::kName));
27   if (node->HasStringAttribute(ax::mojom::StringAttribute::kDescription))
28     strings->push_back(
29         node->GetString16Attribute(ax::mojom::StringAttribute::kDescription));
30   if (node->HasStringAttribute(ax::mojom::StringAttribute::kValue))
31     strings->push_back(
32         node->GetString16Attribute(ax::mojom::StringAttribute::kValue));
33   if (node->HasStringAttribute(ax::mojom::StringAttribute::kPlaceholder))
34     strings->push_back(
35         node->GetString16Attribute(ax::mojom::StringAttribute::kPlaceholder));
36 }
37 
OneShotAccessibilityTreeSearch(BrowserAccessibility * scope)38 OneShotAccessibilityTreeSearch::OneShotAccessibilityTreeSearch(
39     BrowserAccessibility* scope)
40     : tree_(scope->manager()),
41       scope_node_(scope),
42       start_node_(scope),
43       direction_(OneShotAccessibilityTreeSearch::FORWARDS),
44       result_limit_(UNLIMITED_RESULTS),
45       immediate_descendants_only_(false),
46       can_wrap_to_last_element_(false),
47       onscreen_only_(false),
48       did_search_(false) {}
49 
~OneShotAccessibilityTreeSearch()50 OneShotAccessibilityTreeSearch::~OneShotAccessibilityTreeSearch() {}
51 
SetStartNode(BrowserAccessibility * start_node)52 void OneShotAccessibilityTreeSearch::SetStartNode(
53     BrowserAccessibility* start_node) {
54   DCHECK(!did_search_);
55   CHECK(start_node);
56 
57   if (!scope_node_->PlatformGetParent() ||
58       start_node->IsDescendantOf(scope_node_->PlatformGetParent())) {
59     start_node_ = start_node;
60   }
61 }
62 
SetDirection(Direction direction)63 void OneShotAccessibilityTreeSearch::SetDirection(Direction direction) {
64   DCHECK(!did_search_);
65   direction_ = direction;
66 }
67 
SetResultLimit(int result_limit)68 void OneShotAccessibilityTreeSearch::SetResultLimit(int result_limit) {
69   DCHECK(!did_search_);
70   result_limit_ = result_limit;
71 }
72 
SetImmediateDescendantsOnly(bool immediate_descendants_only)73 void OneShotAccessibilityTreeSearch::SetImmediateDescendantsOnly(
74     bool immediate_descendants_only) {
75   DCHECK(!did_search_);
76   immediate_descendants_only_ = immediate_descendants_only;
77 }
78 
SetCanWrapToLastElement(bool can_wrap_to_last_element)79 void OneShotAccessibilityTreeSearch::SetCanWrapToLastElement(
80     bool can_wrap_to_last_element) {
81   DCHECK(!did_search_);
82   can_wrap_to_last_element_ = can_wrap_to_last_element;
83 }
84 
SetOnscreenOnly(bool onscreen_only)85 void OneShotAccessibilityTreeSearch::SetOnscreenOnly(bool onscreen_only) {
86   DCHECK(!did_search_);
87   onscreen_only_ = onscreen_only;
88 }
89 
SetSearchText(const std::string & text)90 void OneShotAccessibilityTreeSearch::SetSearchText(const std::string& text) {
91   DCHECK(!did_search_);
92   search_text_ = text;
93 }
94 
AddPredicate(AccessibilityMatchPredicate predicate)95 void OneShotAccessibilityTreeSearch::AddPredicate(
96     AccessibilityMatchPredicate predicate) {
97   DCHECK(!did_search_);
98   predicates_.push_back(predicate);
99 }
100 
CountMatches()101 size_t OneShotAccessibilityTreeSearch::CountMatches() {
102   if (!did_search_)
103     Search();
104 
105   return matches_.size();
106 }
107 
GetMatchAtIndex(size_t index)108 BrowserAccessibility* OneShotAccessibilityTreeSearch::GetMatchAtIndex(
109     size_t index) {
110   if (!did_search_)
111     Search();
112 
113   CHECK(index < matches_.size());
114   return matches_[index];
115 }
116 
Search()117 void OneShotAccessibilityTreeSearch::Search() {
118   if (immediate_descendants_only_) {
119     SearchByIteratingOverChildren();
120   } else {
121     SearchByWalkingTree();
122   }
123   did_search_ = true;
124 }
125 
SearchByIteratingOverChildren()126 void OneShotAccessibilityTreeSearch::SearchByIteratingOverChildren() {
127   // Iterate over the children of scope_node_.
128   // If start_node_ is specified, iterate over the first child past that
129   // node.
130 
131   uint32_t count = scope_node_->PlatformChildCount();
132   if (count == 0)
133     return;
134 
135   // We only care about immediate children of scope_node_, so walk up
136   // start_node_ until we get to an immediate child. If it isn't a child,
137   // we ignore start_node_.
138   while (start_node_ && start_node_->PlatformGetParent() != scope_node_)
139     start_node_ = start_node_->PlatformGetParent();
140 
141   uint32_t index = (direction_ == FORWARDS ? 0 : count - 1);
142   if (start_node_) {
143     index = start_node_->GetIndexInParent();
144     if (direction_ == FORWARDS)
145       index++;
146     else
147       index--;
148   }
149 
150   while (index < count && (result_limit_ == UNLIMITED_RESULTS ||
151                            static_cast<int>(matches_.size()) < result_limit_)) {
152     BrowserAccessibility* node = scope_node_->PlatformGetChild(index);
153     if (Matches(node))
154       matches_.push_back(node);
155 
156     if (direction_ == FORWARDS)
157       index++;
158     else
159       index--;
160   }
161 }
162 
SearchByWalkingTree()163 void OneShotAccessibilityTreeSearch::SearchByWalkingTree() {
164   BrowserAccessibility* node = nullptr;
165   node = start_node_;
166   if (node != scope_node_ || result_limit_ == 1) {
167     if (direction_ == FORWARDS)
168       node = tree_->NextInTreeOrder(start_node_);
169     else
170       node = tree_->PreviousInTreeOrder(start_node_, can_wrap_to_last_element_);
171   }
172 
173   BrowserAccessibility* stop_node = scope_node_->PlatformGetParent();
174   while (node && node != stop_node &&
175          (result_limit_ == UNLIMITED_RESULTS ||
176           static_cast<int>(matches_.size()) < result_limit_)) {
177     if (Matches(node))
178       matches_.push_back(node);
179 
180     if (direction_ == FORWARDS) {
181       node = tree_->NextInTreeOrder(node);
182     } else {
183       // This needs to be handled carefully. If not, there is a chance of
184       // getting into infinite loop.
185       if (can_wrap_to_last_element_ && !stop_node &&
186           node->manager()->GetRoot() == node) {
187         stop_node = node;
188       }
189       node = tree_->PreviousInTreeOrder(node, can_wrap_to_last_element_);
190     }
191   }
192 }
193 
Matches(BrowserAccessibility * node)194 bool OneShotAccessibilityTreeSearch::Matches(BrowserAccessibility* node) {
195   for (size_t i = 0; i < predicates_.size(); ++i) {
196     if (!predicates_[i](start_node_, node))
197       return false;
198   }
199 
200   if (node->HasState(ax::mojom::State::kInvisible))
201     return false;  // Programmatically hidden, e.g. aria-hidden or via CSS.
202 
203   if (onscreen_only_ && node->IsOffscreen())
204     return false;  // Partly scrolled offscreen.
205 
206   if (!search_text_.empty()) {
207     base::string16 search_text_lower =
208         base::i18n::ToLower(base::UTF8ToUTF16(search_text_));
209     std::vector<base::string16> node_strings;
210     GetNodeStrings(node, &node_strings);
211     bool found_text_match = false;
212     for (size_t i = 0; i < node_strings.size(); ++i) {
213       base::string16 node_string_lower = base::i18n::ToLower(node_strings[i]);
214       if (node_string_lower.find(search_text_lower) != base::string16::npos) {
215         found_text_match = true;
216         break;
217       }
218     }
219     if (!found_text_match)
220       return false;
221   }
222 
223   return true;
224 }
225 
226 //
227 // Predicates
228 //
229 
AccessibilityArticlePredicate(BrowserAccessibility * start,BrowserAccessibility * node)230 bool AccessibilityArticlePredicate(BrowserAccessibility* start,
231                                    BrowserAccessibility* node) {
232   return node->GetRole() == ax::mojom::Role::kArticle;
233 }
234 
AccessibilityButtonPredicate(BrowserAccessibility * start,BrowserAccessibility * node)235 bool AccessibilityButtonPredicate(BrowserAccessibility* start,
236                                   BrowserAccessibility* node) {
237   switch (node->GetRole()) {
238     case ax::mojom::Role::kButton:
239     case ax::mojom::Role::kMenuButton:
240     case ax::mojom::Role::kPopUpButton:
241     case ax::mojom::Role::kSwitch:
242     case ax::mojom::Role::kToggleButton:
243       return true;
244     default:
245       return false;
246   }
247 }
248 
AccessibilityBlockquotePredicate(BrowserAccessibility * start,BrowserAccessibility * node)249 bool AccessibilityBlockquotePredicate(BrowserAccessibility* start,
250                                       BrowserAccessibility* node) {
251   return node->GetRole() == ax::mojom::Role::kBlockquote;
252 }
253 
AccessibilityCheckboxPredicate(BrowserAccessibility * start,BrowserAccessibility * node)254 bool AccessibilityCheckboxPredicate(BrowserAccessibility* start,
255                                     BrowserAccessibility* node) {
256   return (node->GetRole() == ax::mojom::Role::kCheckBox ||
257           node->GetRole() == ax::mojom::Role::kMenuItemCheckBox);
258 }
259 
AccessibilityComboboxPredicate(BrowserAccessibility * start,BrowserAccessibility * node)260 bool AccessibilityComboboxPredicate(BrowserAccessibility* start,
261                                     BrowserAccessibility* node) {
262   return (node->GetRole() == ax::mojom::Role::kComboBoxGrouping ||
263           node->GetRole() == ax::mojom::Role::kComboBoxMenuButton ||
264           node->GetRole() == ax::mojom::Role::kTextFieldWithComboBox ||
265           node->GetRole() == ax::mojom::Role::kPopUpButton);
266 }
267 
AccessibilityControlPredicate(BrowserAccessibility * start,BrowserAccessibility * node)268 bool AccessibilityControlPredicate(BrowserAccessibility* start,
269                                    BrowserAccessibility* node) {
270   if (ui::IsControl(node->GetRole()))
271     return true;
272   if (node->HasState(ax::mojom::State::kFocusable) &&
273       node->GetRole() != ax::mojom::Role::kIframe &&
274       node->GetRole() != ax::mojom::Role::kIframePresentational &&
275       !ui::IsLink(node->GetRole()) &&
276       node->GetRole() != ax::mojom::Role::kWebArea &&
277       node->GetRole() != ax::mojom::Role::kRootWebArea) {
278     return true;
279   }
280   return false;
281 }
282 
AccessibilityFocusablePredicate(BrowserAccessibility * start,BrowserAccessibility * node)283 bool AccessibilityFocusablePredicate(BrowserAccessibility* start,
284                                      BrowserAccessibility* node) {
285   bool focusable = node->HasState(ax::mojom::State::kFocusable);
286   if (node->GetRole() == ax::mojom::Role::kIframe ||
287       node->GetRole() == ax::mojom::Role::kIframePresentational ||
288       node->GetRole() == ax::mojom::Role::kWebArea ||
289       node->GetRole() == ax::mojom::Role::kRootWebArea) {
290     focusable = false;
291   }
292   return focusable;
293 }
294 
AccessibilityGraphicPredicate(BrowserAccessibility * start,BrowserAccessibility * node)295 bool AccessibilityGraphicPredicate(BrowserAccessibility* start,
296                                    BrowserAccessibility* node) {
297   return ui::IsImageOrVideo(node->GetRole());
298 }
299 
AccessibilityHeadingPredicate(BrowserAccessibility * start,BrowserAccessibility * node)300 bool AccessibilityHeadingPredicate(BrowserAccessibility* start,
301                                    BrowserAccessibility* node) {
302   return ui::IsHeading(node->GetRole());
303 }
304 
AccessibilityH1Predicate(BrowserAccessibility * start,BrowserAccessibility * node)305 bool AccessibilityH1Predicate(BrowserAccessibility* start,
306                               BrowserAccessibility* node) {
307   return (ui::IsHeading(node->GetRole()) &&
308           node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
309               1);
310 }
311 
AccessibilityH2Predicate(BrowserAccessibility * start,BrowserAccessibility * node)312 bool AccessibilityH2Predicate(BrowserAccessibility* start,
313                               BrowserAccessibility* node) {
314   return (ui::IsHeading(node->GetRole()) &&
315           node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
316               2);
317 }
318 
AccessibilityH3Predicate(BrowserAccessibility * start,BrowserAccessibility * node)319 bool AccessibilityH3Predicate(BrowserAccessibility* start,
320                               BrowserAccessibility* node) {
321   return (ui::IsHeading(node->GetRole()) &&
322           node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
323               3);
324 }
325 
AccessibilityH4Predicate(BrowserAccessibility * start,BrowserAccessibility * node)326 bool AccessibilityH4Predicate(BrowserAccessibility* start,
327                               BrowserAccessibility* node) {
328   return (ui::IsHeading(node->GetRole()) &&
329           node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
330               4);
331 }
332 
AccessibilityH5Predicate(BrowserAccessibility * start,BrowserAccessibility * node)333 bool AccessibilityH5Predicate(BrowserAccessibility* start,
334                               BrowserAccessibility* node) {
335   return (ui::IsHeading(node->GetRole()) &&
336           node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
337               5);
338 }
339 
AccessibilityH6Predicate(BrowserAccessibility * start,BrowserAccessibility * node)340 bool AccessibilityH6Predicate(BrowserAccessibility* start,
341                               BrowserAccessibility* node) {
342   return (ui::IsHeading(node->GetRole()) &&
343           node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
344               6);
345 }
346 
AccessibilityHeadingSameLevelPredicate(BrowserAccessibility * start,BrowserAccessibility * node)347 bool AccessibilityHeadingSameLevelPredicate(BrowserAccessibility* start,
348                                             BrowserAccessibility* node) {
349   return (
350       node->GetRole() == ax::mojom::Role::kHeading &&
351       start->GetRole() == ax::mojom::Role::kHeading &&
352       (node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
353        start->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel)));
354 }
355 
AccessibilityFramePredicate(BrowserAccessibility * start,BrowserAccessibility * node)356 bool AccessibilityFramePredicate(BrowserAccessibility* start,
357                                  BrowserAccessibility* node) {
358   if (node->IsWebAreaForPresentationalIframe())
359     return false;
360   if (!node->PlatformGetParent())
361     return false;
362   return (node->GetRole() == ax::mojom::Role::kWebArea ||
363           node->GetRole() == ax::mojom::Role::kRootWebArea);
364 }
365 
AccessibilityLandmarkPredicate(BrowserAccessibility * start,BrowserAccessibility * node)366 bool AccessibilityLandmarkPredicate(BrowserAccessibility* start,
367                                     BrowserAccessibility* node) {
368   switch (node->GetRole()) {
369     case ax::mojom::Role::kApplication:
370     case ax::mojom::Role::kArticle:
371     case ax::mojom::Role::kBanner:
372     case ax::mojom::Role::kComplementary:
373     case ax::mojom::Role::kContentInfo:
374     case ax::mojom::Role::kMain:
375     case ax::mojom::Role::kNavigation:
376     case ax::mojom::Role::kRegion:
377     case ax::mojom::Role::kSearch:
378     case ax::mojom::Role::kSection:
379       return true;
380     default:
381       return false;
382   }
383 }
384 
AccessibilityLinkPredicate(BrowserAccessibility * start,BrowserAccessibility * node)385 bool AccessibilityLinkPredicate(BrowserAccessibility* start,
386                                 BrowserAccessibility* node) {
387   return ui::IsLink(node->GetRole());
388 }
389 
AccessibilityListPredicate(BrowserAccessibility * start,BrowserAccessibility * node)390 bool AccessibilityListPredicate(BrowserAccessibility* start,
391                                 BrowserAccessibility* node) {
392   return ui::IsList(node->GetRole());
393 }
394 
AccessibilityListItemPredicate(BrowserAccessibility * start,BrowserAccessibility * node)395 bool AccessibilityListItemPredicate(BrowserAccessibility* start,
396                                     BrowserAccessibility* node) {
397   return ui::IsListItem(node->GetRole());
398 }
399 
AccessibilityLiveRegionPredicate(BrowserAccessibility * start,BrowserAccessibility * node)400 bool AccessibilityLiveRegionPredicate(BrowserAccessibility* start,
401                                       BrowserAccessibility* node) {
402   return node->HasStringAttribute(ax::mojom::StringAttribute::kLiveStatus);
403 }
404 
AccessibilityMainPredicate(BrowserAccessibility * start,BrowserAccessibility * node)405 bool AccessibilityMainPredicate(BrowserAccessibility* start,
406                                 BrowserAccessibility* node) {
407   return (node->GetRole() == ax::mojom::Role::kMain);
408 }
409 
AccessibilityMediaPredicate(BrowserAccessibility * start,BrowserAccessibility * node)410 bool AccessibilityMediaPredicate(BrowserAccessibility* start,
411                                  BrowserAccessibility* node) {
412   const std::string& tag =
413       node->GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag);
414   return tag == "audio" || tag == "video";
415 }
416 
AccessibilityPopupButtonPredicate(BrowserAccessibility * start,BrowserAccessibility * node)417 bool AccessibilityPopupButtonPredicate(BrowserAccessibility* start,
418                                        BrowserAccessibility* node) {
419   return (node->GetRole() == ax::mojom::Role::kPopUpButton);
420 }
421 
AccessibilityRadioButtonPredicate(BrowserAccessibility * start,BrowserAccessibility * node)422 bool AccessibilityRadioButtonPredicate(BrowserAccessibility* start,
423                                        BrowserAccessibility* node) {
424   return (node->GetRole() == ax::mojom::Role::kRadioButton ||
425           node->GetRole() == ax::mojom::Role::kMenuItemRadio);
426 }
427 
AccessibilityRadioGroupPredicate(BrowserAccessibility * start,BrowserAccessibility * node)428 bool AccessibilityRadioGroupPredicate(BrowserAccessibility* start,
429                                       BrowserAccessibility* node) {
430   return node->GetRole() == ax::mojom::Role::kRadioGroup;
431 }
432 
AccessibilityTablePredicate(BrowserAccessibility * start,BrowserAccessibility * node)433 bool AccessibilityTablePredicate(BrowserAccessibility* start,
434                                  BrowserAccessibility* node) {
435   return ui::IsTableLike(node->GetRole());
436 }
437 
AccessibilityTextfieldPredicate(BrowserAccessibility * start,BrowserAccessibility * node)438 bool AccessibilityTextfieldPredicate(BrowserAccessibility* start,
439                                      BrowserAccessibility* node) {
440   return (node->IsPlainTextField() || node->IsRichTextField());
441 }
442 
AccessibilityTextStyleBoldPredicate(BrowserAccessibility * start,BrowserAccessibility * node)443 bool AccessibilityTextStyleBoldPredicate(BrowserAccessibility* start,
444                                          BrowserAccessibility* node) {
445   return node->GetData().HasTextStyle(ax::mojom::TextStyle::kBold);
446 }
447 
AccessibilityTextStyleItalicPredicate(BrowserAccessibility * start,BrowserAccessibility * node)448 bool AccessibilityTextStyleItalicPredicate(BrowserAccessibility* start,
449                                            BrowserAccessibility* node) {
450   return node->GetData().HasTextStyle(ax::mojom::TextStyle::kItalic);
451 }
452 
AccessibilityTextStyleUnderlinePredicate(BrowserAccessibility * start,BrowserAccessibility * node)453 bool AccessibilityTextStyleUnderlinePredicate(BrowserAccessibility* start,
454                                               BrowserAccessibility* node) {
455   return node->GetData().HasTextStyle(ax::mojom::TextStyle::kUnderline);
456 }
457 
AccessibilityTreePredicate(BrowserAccessibility * start,BrowserAccessibility * node)458 bool AccessibilityTreePredicate(BrowserAccessibility* start,
459                                 BrowserAccessibility* node) {
460   return (node->IsPlainTextField() || node->IsRichTextField());
461 }
462 
AccessibilityUnvisitedLinkPredicate(BrowserAccessibility * start,BrowserAccessibility * node)463 bool AccessibilityUnvisitedLinkPredicate(BrowserAccessibility* start,
464                                          BrowserAccessibility* node) {
465   return node->GetRole() == ax::mojom::Role::kLink &&
466          !node->HasState(ax::mojom::State::kVisited);
467 }
468 
AccessibilityVisitedLinkPredicate(BrowserAccessibility * start,BrowserAccessibility * node)469 bool AccessibilityVisitedLinkPredicate(BrowserAccessibility* start,
470                                        BrowserAccessibility* node) {
471   return node->GetRole() == ax::mojom::Role::kLink &&
472          node->HasState(ax::mojom::State::kVisited);
473 }
474 
475 }  // namespace content
476