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