| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/accessibility/ax_relation_cache.h" |
| |
| #include "base/memory/ptr_util.h" |
| #include "third_party/blink/renderer/core/dom/element_traversal.h" |
| #include "third_party/blink/renderer/core/html/forms/html_label_element.h" |
| |
| namespace blink { |
| |
| AXRelationCache::AXRelationCache(AXObjectCacheImpl* object_cache) |
| : object_cache_(object_cache) {} |
| |
| AXRelationCache::~AXRelationCache() = default; |
| |
| void AXRelationCache::DoInitialDocumentScan() { |
| // Init the relation cache with elements already in the document. |
| Document& document = object_cache_->GetDocument(); |
| for (Element& element : |
| ElementTraversal::DescendantsOf(*document.documentElement())) { |
| const auto& id = element.FastGetAttribute(html_names::kForAttr); |
| if (!id.IsEmpty()) |
| all_previously_seen_label_target_ids_.insert(id); |
| |
| // Ensure correct ancestor chains even when not all AXObject's in the |
| // document are created, e.g. in the devtools accessibility panel. |
| // Defers adding aria-owns targets as children of their new parents, |
| // and to the relation cache, until the appropriate document lifecycle. |
| #if DCHECK_IS_ON() |
| DCHECK(document.Lifecycle().GetState() >= DocumentLifecycle::kLayoutClean) |
| << "Unclean document at lifecycle " << document.Lifecycle().ToString(); |
| #endif |
| if (element.FastHasAttribute(html_names::kAriaOwnsAttr)) { |
| if (AXObject* owner = GetOrCreate(&element, nullptr)) { |
| owner_ids_to_update_.insert(owner->AXObjectID()); |
| } |
| } |
| } |
| |
| initialized_ = true; |
| } |
| |
| void AXRelationCache::ProcessUpdatesWithCleanLayout() { |
| if (!initialized_) |
| DoInitialDocumentScan(); |
| |
| HashSet<AXID> old_owner_ids_to_update; |
| old_owner_ids_to_update.swap(owner_ids_to_update_); |
| |
| for (AXID aria_owns_obj_id : old_owner_ids_to_update) { |
| AXObject* obj = ObjectFromAXID(aria_owns_obj_id); |
| if (obj) |
| UpdateAriaOwnsWithCleanLayout(obj); |
| } |
| |
| // TODO(1301117): this is a workaround to avoid an infinite loop. |
| // owner_ids_to_update_ is modified in calls to |
| // UpdateAriaOwnsWithCleanLayout and add again AXIDs that will end up |
| // looping forever in AXObjectCacheImpl::ProcessDeferredAccessibilityEvents |
| owner_ids_to_update_.clear(); |
| } |
| |
| bool AXRelationCache::IsAriaOwned(const AXObject* child) const { |
| return child && |
| aria_owned_child_to_owner_mapping_.Contains(child->AXObjectID()); |
| } |
| |
| AXObject* AXRelationCache::GetAriaOwnedParent(const AXObject* child) const { |
| // Child IDs may still be present in owning parents whose list of children |
| // have been marked as requiring an update, but have not been updated yet. |
| HashMap<AXID, AXID>::const_iterator iter = |
| aria_owned_child_to_owner_mapping_.find(child->AXObjectID()); |
| if (iter == aria_owned_child_to_owner_mapping_.end()) |
| return nullptr; |
| return ObjectFromAXID(iter->value); |
| } |
| |
| // Update reverse relation map, where relation_source is related to target_ids. |
| void AXRelationCache::UpdateReverseRelations(const AXObject* relation_source, |
| const Vector<String>& target_ids) { |
| AXID relation_source_axid = relation_source->AXObjectID(); |
| |
| // Add entries to reverse map. |
| for (const String& target_id : target_ids) { |
| auto result = |
| id_attr_to_related_mapping_.insert(target_id, HashSet<AXID>()); |
| result.stored_value->value.insert(relation_source_axid); |
| } |
| } |
| |
| static bool ContainsCycle(AXObject* owner, AXObject* child) { |
| // Walk up the parents of the owner object, make sure that this child |
| // doesn't appear there, as that would create a cycle. |
| for (AXObject* parent = owner; parent; parent = parent->ParentObject()) { |
| if (parent == child) |
| return true; |
| } |
| return false; |
| } |
| |
| bool AXRelationCache::IsValidOwnsRelation(AXObject* owner, |
| AXObject* child) const { |
| if (!IsValidOwner(owner) || !IsValidOwnedChild(child)) |
| return false; |
| |
| // If this child is already aria-owned by a different owner, continue. |
| // It's an author error if this happens and we don't worry about which of |
| // the two owners wins ownership, as long as only one of them does. |
| if (IsAriaOwned(child) && GetAriaOwnedParent(child) != owner) |
| return false; |
| |
| // You can't own yourself or an ancestor! |
| if (ContainsCycle(owner, child)) |
| return false; |
| |
| return true; |
| } |
| |
| bool AXRelationCache::IsValidOwner(AXObject* owner) { |
| if (!owner->GetNode()) { |
| NOTREACHED() << "Cannot use aria-owns without a node on both ends"; |
| return false; |
| } |
| |
| // Can't have children. |
| if (!owner->CanHaveChildren()) |
| return false; |
| |
| // No aria-owns in editable controlsm otherwise wreaks havoc. |
| if (owner->IsNativeTextControl() || owner->HasContentEditableAttributeSet()) |
| return false; |
| |
| // Image maps can only use <img usemap> to "own" <area> children. |
| // This requires special parenting logic, and aria-owns is prevented here in |
| // order to keep things from getting too complex. |
| if (owner->RoleValue() == ax::mojom::blink::Role::kImageMap) |
| return false; |
| |
| // Similarly, do not allow <area> to own another object. |
| if (owner->IsImageMapLink()) |
| return false; |
| |
| return true; |
| } |
| |
| bool AXRelationCache::IsValidOwnedChild(AXObject* child) { |
| if (!child) |
| return false; |
| |
| if (!child->GetNode()) { |
| NOTREACHED() << "Cannot use aria-owns without a node on both ends"; |
| return false; |
| } |
| |
| if (child->IsImageMapLink()) |
| return false; // An area can't be owned, only parented by <img usemap>. |
| |
| return true; |
| } |
| |
| void AXRelationCache::UnmapOwnedChildren(const AXObject* owner, |
| const Vector<AXID> child_ids) { |
| for (AXID removed_child_id : child_ids) { |
| // Find the AXObject for the child that this owner no longer owns. |
| AXObject* removed_child = ObjectFromAXID(removed_child_id); |
| |
| // It's possible that this child has already been owned by some other |
| // owner, in which case we don't need to do anything. |
| if (removed_child && GetAriaOwnedParent(removed_child) != owner) |
| continue; |
| |
| // Remove it from the child -> owner mapping so it's not owned by this |
| // owner anymore. |
| aria_owned_child_to_owner_mapping_.erase(removed_child_id); |
| |
| if (removed_child) { |
| // If the child still exists, find its "real" parent, and reparent it |
| // back to its real parent in the tree by detaching it from its current |
| // parent and calling childrenChanged on its real parent. |
| removed_child->DetachFromParent(); |
| // Recompute the real parent and cache it. |
| AXObject* real_parent = removed_child->ParentObject(); |
| ChildrenChanged(real_parent); |
| } |
| } |
| } |
| |
| void AXRelationCache::MapOwnedChildren(const AXObject* owner, |
| const Vector<AXID> child_ids) { |
| for (AXID added_child_id : child_ids) { |
| AXObject* added_child = ObjectFromAXID(added_child_id); |
| |
| // Add this child to the mapping from child to owner. |
| aria_owned_child_to_owner_mapping_.Set(added_child_id, owner->AXObjectID()); |
| |
| // Now detach the object from its original parent and call childrenChanged |
| // on the original parent so that it can recompute its list of children. |
| AXObject* original_parent = added_child->ParentObject(); |
| added_child->DetachFromParent(); |
| added_child->SetParent(const_cast<AXObject*>(owner)); |
| ChildrenChanged(original_parent); |
| } |
| } |
| |
| void AXRelationCache::UpdateAriaOwnsFromAttrAssociatedElementsWithCleanLayout( |
| AXObject* owner, |
| const HeapVector<Member<Element>>& attr_associated_elements, |
| HeapVector<Member<AXObject>>& validated_owned_children_result) { |
| // attr-associated elements have already had their scope validated, but they |
| // need to be further validated to determine if they introduce a cycle or are |
| // already owned by another element. |
| |
| Vector<String> owned_id_vector; |
| for (const auto& element : attr_associated_elements) { |
| // Pass in owner parent assuming that the owns relationship will be valid. |
| // It will be cleared below if the owns relationship is found to be invalid. |
| AXObject* child = GetOrCreate(element, owner); |
| |
| // TODO(meredithl): Determine how to update reverse relations for elements |
| // without an id. |
| if (element->GetIdAttribute()) |
| owned_id_vector.push_back(element->GetIdAttribute()); |
| if (IsValidOwnsRelation(const_cast<AXObject*>(owner), child)) { |
| validated_owned_children_result.push_back(child); |
| } else if (child) { |
| // Invalid owns relation: repair the parent that was set above. |
| child->SetParent(child->ComputeParentImpl()); |
| } |
| } |
| |
| // Track reverse relations for future tree updates. |
| UpdateReverseRelations(owner, owned_id_vector); |
| |
| // Update the internal mappings of owned children. |
| UpdateAriaOwnerToChildrenMappingWithCleanLayout( |
| owner, validated_owned_children_result); |
| } |
| |
| void AXRelationCache::GetAriaOwnedChildren( |
| const AXObject* owner, |
| HeapVector<Member<AXObject>>& validated_owned_children_result) { |
| if (!aria_owner_to_children_mapping_.Contains(owner->AXObjectID())) |
| return; |
| Vector<AXID> current_child_axids = |
| aria_owner_to_children_mapping_.at(owner->AXObjectID()); |
| for (AXID child_id : current_child_axids) { |
| AXObject* child = ObjectFromAXID(child_id); |
| if (child) { |
| validated_owned_children_result.push_back(child); |
| DCHECK(IsAriaOwned(child)) << "Owned child not in owned child map"; |
| } |
| } |
| } |
| |
| void AXRelationCache::UpdateAriaOwnsWithCleanLayout(AXObject* owner) { |
| Element* element = owner->GetElement(); |
| if (!element) |
| return; |
| |
| DCHECK(!element->GetDocument().NeedsLayoutTreeUpdateForNode(*element)); |
| |
| Vector<String> owned_id_vector; |
| owner->TokenVectorFromAttribute(owned_id_vector, html_names::kAriaOwnsAttr); |
| |
| // Track reverse relations for future tree updates. |
| UpdateReverseRelations(owner, owned_id_vector); |
| |
| // We first check if the element has an explicitly set aria-owns association. |
| // Explicitly set elements are validated on setting time (that they are in a |
| // valid scope etc). The content attribute can contain ids that are not |
| // legally ownable. |
| HeapVector<Member<AXObject>> owned_children; |
| if (element && element->HasExplicitlySetAttrAssociatedElements( |
| html_names::kAriaOwnsAttr)) { |
| UpdateAriaOwnsFromAttrAssociatedElementsWithCleanLayout( |
| owner, |
| element->GetElementArrayAttribute(html_names::kAriaOwnsAttr).value(), |
| owned_children); |
| } else { |
| // Figure out the ids that actually correspond to children that exist |
| // and that we can legally own (not cyclical, not already owned, etc.) and |
| // update the maps and |validated_owned_children_result| based on that. |
| // |
| // Figure out the children that are owned by this object and are in the |
| // tree. |
| TreeScope& scope = element->GetTreeScope(); |
| Vector<AXID> validated_owned_child_axids; |
| for (const String& id_name : owned_id_vector) { |
| Element* child_element = scope.getElementById(AtomicString(id_name)); |
| // Pass in owner parent assuming that the owns relationship will be valid. |
| // It will be cleared below if the owns relationship is found to be |
| // invalid. |
| AXObject* child = GetOrCreate(child_element, owner); |
| if (IsValidOwnsRelation(const_cast<AXObject*>(owner), child)) { |
| owned_children.push_back(child); |
| } else if (child) { |
| // Invalid owns relation: repair the parent that was set above. |
| child->SetParent(child->ComputeParentImpl()); |
| } |
| } |
| } |
| |
| // Update the internal validated mapping of owned children. This will |
| // fire an event if the mapping has changed. |
| UpdateAriaOwnerToChildrenMappingWithCleanLayout(owner, owned_children); |
| } |
| |
| void AXRelationCache::UpdateAriaOwnerToChildrenMappingWithCleanLayout( |
| AXObject* owner, |
| HeapVector<Member<AXObject>>& validated_owned_children_result) { |
| Vector<AXID> validated_owned_child_axids; |
| for (auto& child : validated_owned_children_result) |
| validated_owned_child_axids.push_back(child->AXObjectID()); |
| |
| // Compare this to the current list of owned children, and exit early if |
| // there are no changes. |
| Vector<AXID> current_child_axids = |
| aria_owner_to_children_mapping_.at(owner->AXObjectID()); |
| if (current_child_axids == validated_owned_child_axids) |
| return; |
| |
| // The list of owned children has changed. Even if they were just reordered, |
| // to be safe and handle all cases we remove all of the current owned |
| // children and add the new list of owned children. |
| UnmapOwnedChildren(owner, current_child_axids); |
| MapOwnedChildren(owner, validated_owned_child_axids); |
| |
| #if DCHECK_IS_ON() |
| // Owned children must be in tree to avoid serialization issues. |
| for (AXObject* child : validated_owned_children_result) { |
| DCHECK(child->AccessibilityIsIncludedInTree()) |
| << "Owned child not in tree: " << child->ToString(true, false) |
| << "\nRecompute included in tree: " |
| << child->ComputeAccessibilityIsIgnoredButIncludedInTree(); |
| } |
| #endif |
| |
| // Finally, update the mapping from the owner to the list of child IDs. |
| aria_owner_to_children_mapping_.Set(owner->AXObjectID(), |
| validated_owned_child_axids); |
| |
| ChildrenChanged(owner); |
| owner->UpdateChildrenIfNecessary(); |
| } |
| |
| bool AXRelationCache::MayHaveHTMLLabelViaForAttribute( |
| const HTMLElement& labelable) { |
| const AtomicString& id = labelable.GetIdAttribute(); |
| if (id.IsEmpty()) |
| return false; |
| return all_previously_seen_label_target_ids_.Contains(id); |
| } |
| |
| // Fill source_objects with AXObjects for relations pointing to target. |
| void AXRelationCache::GetReverseRelated( |
| Node* target, |
| HeapVector<Member<AXObject>>& source_objects) { |
| auto* element = DynamicTo<Element>(target); |
| if (!element) |
| return; |
| |
| if (!element->HasID()) |
| return; |
| |
| auto it = id_attr_to_related_mapping_.find(element->GetIdAttribute()); |
| if (it == id_attr_to_related_mapping_.end()) |
| return; |
| |
| for (const auto& source_axid : it->value) { |
| AXObject* source_object = ObjectFromAXID(source_axid); |
| if (source_object) |
| source_objects.push_back(source_object); |
| } |
| } |
| |
| void AXRelationCache::UpdateRelatedTree(Node* node, AXObject* obj) { |
| HeapVector<Member<AXObject>> related_sources; |
| #if DCHECK_IS_ON() |
| DCHECK(node); |
| AXObject* obj_for_node = Get(node); |
| DCHECK(!obj || obj_for_node == obj) |
| << "Object and node did not match:" |
| << "\n* node = " << node << "\n* obj = " << obj->ToString(true, true) |
| << "\n* obj_for_node = " |
| << (obj_for_node ? obj_for_node->ToString(true, true) : "null"); |
| #endif |
| AXObject* related_target = obj ? obj : Get(node); |
| // If it's already owned, schedule an update on the owner. |
| if (related_target && IsAriaOwned(related_target)) { |
| AXObject* owned_parent = GetAriaOwnedParent(related_target); |
| DCHECK(owned_parent); |
| owner_ids_to_update_.insert(owned_parent->AXObjectID()); |
| } |
| |
| // Ensure children are updated if there is a change. |
| GetReverseRelated(node, related_sources); |
| for (AXObject* related : related_sources) { |
| if (related) { |
| owner_ids_to_update_.insert(related->AXObjectID()); |
| ChildrenChanged(related); |
| } |
| } |
| |
| UpdateRelatedText(node); |
| } |
| |
| void AXRelationCache::UpdateRelatedText(Node* node) { |
| // Walk up ancestor chain from node and refresh text of any related content. |
| // TODO(crbug.com/1109265): It's very likely this loop should only walk the |
| // unignored AXObject chain, but doing so breaks a number of tests related to |
| // name or description computation / invalidation. |
| for (Node* current_node = node; current_node; |
| current_node = current_node->parentNode()) { |
| // Reverse relations via aria-labelledby, aria-describedby, aria-owns. |
| HeapVector<Member<AXObject>> related_sources; |
| GetReverseRelated(current_node, related_sources); |
| for (AXObject* related : related_sources) { |
| if (related) |
| object_cache_->MarkAXObjectDirty(related, /*subtree=*/false); |
| } |
| |
| // Ancestors that may derive their accessible name from descendant content |
| // should also handle text changed events when descendant content changes. |
| if (current_node != node) { |
| AXObject* obj = Get(current_node); |
| if (obj && obj->SupportsNameFromContents(/*recursive=*/false)) |
| object_cache_->MarkAXObjectDirty(obj, /*subtree=*/false); |
| } |
| |
| // Forward relation via <label for="[id]">. |
| if (IsA<HTMLLabelElement>(*current_node)) |
| LabelChanged(current_node); |
| } |
| } |
| |
| void AXRelationCache::RemoveAXID(AXID obj_id) { |
| // Need to remove from maps. |
| // There are maps from children to their owners, and owners to their children. |
| // In addition, the removed id may be an owner, or be owned, or both. |
| |
| // |obj_id| owned others: |
| if (aria_owner_to_children_mapping_.Contains(obj_id)) { |
| // |obj_id| longer owns anything. |
| Vector<AXID> child_axids = aria_owner_to_children_mapping_.at(obj_id); |
| aria_owned_child_to_owner_mapping_.RemoveAll(child_axids); |
| // Owned children are no longer owned by |obj_id| |
| aria_owner_to_children_mapping_.erase(obj_id); |
| } |
| |
| // Another id owned |obj_id|: |
| if (aria_owned_child_to_owner_mapping_.Contains(obj_id)) { |
| // Previous owner no longer relevant to this child. |
| // Also, remove |obj_id| from previous owner's owned child list: |
| AXID owner_id = aria_owned_child_to_owner_mapping_.Take(obj_id); |
| const Vector<AXID>& owners_owned_children = |
| aria_owner_to_children_mapping_.at(owner_id); |
| for (wtf_size_t index = 0; index < owners_owned_children.size(); index++) { |
| if (owners_owned_children[index] == obj_id) { |
| aria_owner_to_children_mapping_.at(owner_id).EraseAt(index); |
| break; |
| } |
| } |
| } |
| } |
| |
| AXObject* AXRelationCache::ObjectFromAXID(AXID axid) const { |
| return object_cache_->ObjectFromAXID(axid); |
| } |
| |
| AXObject* AXRelationCache::Get(Node* node) { |
| return object_cache_->Get(node); |
| } |
| |
| AXObject* AXRelationCache::GetOrCreate(Node* node, const AXObject* owner) { |
| return object_cache_->GetOrCreate(node, const_cast<AXObject*>(owner)); |
| } |
| |
| void AXRelationCache::ChildrenChanged(AXObject* object) { |
| object->ChildrenChanged(); |
| } |
| |
| void AXRelationCache::LabelChanged(Node* node) { |
| const auto& id = |
| To<HTMLElement>(node)->FastGetAttribute(html_names::kForAttr); |
| if (!id.IsEmpty()) { |
| all_previously_seen_label_target_ids_.insert(id); |
| if (auto* control = To<HTMLLabelElement>(node)->control()) { |
| if (AXObject* obj = Get(control)) |
| object_cache_->MarkAXObjectDirty(obj, /*subtree=*/false); |
| } |
| } |
| } |
| |
| } // namespace blink |