| /* |
| * Copyright (C) 2006, 2007 Apple Inc. All rights reserved. |
| * Copyright (C) 2010 Igalia S.L |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "third_party/blink/renderer/core/page/context_menu_controller.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "services/metrics/public/cpp/ukm_entry_builder.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "third_party/blink/public/common/context_menu_data/context_menu_data.h" |
| #include "third_party/blink/public/common/context_menu_data/edit_flags.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/input/web_menu_source_type.h" |
| #include "third_party/blink/public/mojom/context_menu/context_menu.mojom-blink.h" |
| #include "third_party/blink/public/platform/impression_conversions.h" |
| #include "third_party/blink/public/web/web_local_frame_client.h" |
| #include "third_party/blink/public/web/web_plugin.h" |
| #include "third_party/blink/public/web/web_text_check_client.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/element_traversal.h" |
| #include "third_party/blink/renderer/core/dom/events/event_target.h" |
| #include "third_party/blink/renderer/core/dom/node.h" |
| #include "third_party/blink/renderer/core/editing/editing_tri_state.h" |
| #include "third_party/blink/renderer/core/editing/editor.h" |
| #include "third_party/blink/renderer/core/editing/ephemeral_range.h" |
| #include "third_party/blink/renderer/core/editing/frame_selection.h" |
| #include "third_party/blink/renderer/core/editing/ime/input_method_controller.h" |
| #include "third_party/blink/renderer/core/editing/selection_controller.h" |
| #include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h" |
| #include "third_party/blink/renderer/core/events/mouse_event.h" |
| #include "third_party/blink/renderer/core/exported/web_plugin_container_impl.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/picture_in_picture_controller.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/frame/web_frame_widget_impl.h" |
| #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" |
| #include "third_party/blink/renderer/core/html/conversion_measurement_parsing.h" |
| #include "third_party/blink/renderer/core/html/forms/html_form_element.h" |
| #include "third_party/blink/renderer/core/html/forms/html_input_element.h" |
| #include "third_party/blink/renderer/core/html/html_anchor_element.h" |
| #include "third_party/blink/renderer/core/html/html_document.h" |
| #include "third_party/blink/renderer/core/html/html_frame_element_base.h" |
| #include "third_party/blink/renderer/core/html/html_image_element.h" |
| #include "third_party/blink/renderer/core/html/html_plugin_element.h" |
| #include "third_party/blink/renderer/core/html/media/html_media_element.h" |
| #include "third_party/blink/renderer/core/input/context_menu_allowed_scope.h" |
| #include "third_party/blink/renderer/core/input/event_handler.h" |
| #include "third_party/blink/renderer/core/input_type_names.h" |
| #include "third_party/blink/renderer/core/layout/layout_embedded_content.h" |
| #include "third_party/blink/renderer/core/loader/document_loader.h" |
| #include "third_party/blink/renderer/core/page/context_menu_provider.h" |
| #include "third_party/blink/renderer/core/page/focus_controller.h" |
| #include "third_party/blink/renderer/core/page/page.h" |
| #include "third_party/blink/renderer/core/page/scrolling/text_fragment_selector_generator.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer.h" |
| #include "third_party/blink/renderer/platform/exported/wrapped_resource_response.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Returns true if node or any of its ancestors have a context menu event |
| // listener. Uses already_visited_nodes to track nodes which have already |
| // been checked across multiple calls to this function, which could cause |
| // the output to be false despite having an ancestor context menu listener. |
| bool UnvisitedNodeOrAncestorHasContextMenuListener( |
| Node* node, |
| HeapHashSet<Member<Node>>& already_visited_nodes) { |
| Node* current_node_for_parent_traversal = node; |
| while (current_node_for_parent_traversal != nullptr) { |
| if (current_node_for_parent_traversal->HasEventListeners( |
| event_type_names::kContextmenu)) { |
| return true; |
| } |
| // If we've already checked this node, all of its ancestors must not |
| // have had listeners (or, we already detected a listener and broke out |
| // early). |
| if (!already_visited_nodes.insert(current_node_for_parent_traversal) |
| .is_new_entry) { |
| break; |
| } |
| current_node_for_parent_traversal = |
| current_node_for_parent_traversal->parentNode(); |
| } |
| return false; |
| } |
| |
| void MaybeRecordImageSelectionUkm( |
| ukm::SourceId source_id, |
| ContextMenuController::ImageSelectionOutcome outcome) { |
| DCHECK_NE(source_id, ukm::kInvalidSourceId); |
| static bool enable = base::GetFieldTrialParamByFeatureAsInt( |
| features::kEnablePenetratingImageSelection, "logUkm", false); |
| |
| if (enable) { |
| ukm::UkmEntryBuilder builder(source_id, "Blink.ContextMenu.ImageSelection"); |
| builder.SetMetric("Outcome", outcome); |
| builder.Record(ukm::UkmRecorder::Get()); |
| } |
| } |
| |
| } // namespace |
| |
| ContextMenuController::ContextMenuController(Page* page) : page_(page) {} |
| |
| ContextMenuController::~ContextMenuController() = default; |
| |
| void ContextMenuController::Trace(Visitor* visitor) const { |
| visitor->Trace(page_); |
| visitor->Trace(menu_provider_); |
| visitor->Trace(hit_test_result_); |
| visitor->Trace(context_menu_client_receiver_); |
| } |
| |
| void ContextMenuController::ClearContextMenu() { |
| if (menu_provider_) |
| menu_provider_->ContextMenuCleared(); |
| menu_provider_ = nullptr; |
| context_menu_client_receiver_.reset(); |
| hit_test_result_ = HitTestResult(); |
| } |
| |
| void ContextMenuController::DocumentDetached(Document* document) { |
| if (Node* inner_node = hit_test_result_.InnerNode()) { |
| // Invalidate the context menu info if its target document is detached. |
| if (inner_node->GetDocument() == document) |
| ClearContextMenu(); |
| } |
| } |
| |
| void ContextMenuController::HandleContextMenuEvent(MouseEvent* mouse_event) { |
| DCHECK(mouse_event->type() == event_type_names::kContextmenu); |
| LocalFrame* frame = mouse_event->target()->ToNode()->GetDocument().GetFrame(); |
| PhysicalOffset location = PhysicalOffset::FromFloatPointRound( |
| FloatPoint(mouse_event->AbsoluteLocation())); |
| |
| if (ShowContextMenu(frame, location, mouse_event->GetMenuSourceType(), |
| mouse_event)) |
| mouse_event->SetDefaultHandled(); |
| } |
| |
| void ContextMenuController::ShowContextMenuAtPoint( |
| LocalFrame* frame, |
| float x, |
| float y, |
| ContextMenuProvider* menu_provider) { |
| menu_provider_ = menu_provider; |
| if (!ShowContextMenu(frame, PhysicalOffset(LayoutUnit(x), LayoutUnit(y)), |
| kMenuSourceNone)) |
| ClearContextMenu(); |
| } |
| |
| void ContextMenuController::CustomContextMenuItemSelected(unsigned action) { |
| if (!menu_provider_) |
| return; |
| menu_provider_->ContextMenuItemSelected(action); |
| ClearContextMenu(); |
| } |
| |
| Node* ContextMenuController::GetContextMenuNodeWithImageContents( |
| const bool report_histograms) { |
| uint32_t outcome = 0; |
| uint32_t hit_test_depth = 0; |
| LocalFrame* top_hit_frame = |
| hit_test_result_.InnerNode()->GetDocument().GetFrame(); |
| Node* found_image_node = nullptr; |
| HeapHashSet<Member<Node>> already_visited_nodes_for_context_menu_listener; |
| |
| for (const auto& raw_node : hit_test_result_.ListBasedTestResult()) { |
| hit_test_depth++; |
| Node* node = raw_node.Get(); |
| |
| // Execute context menu listener and cross frame checks before image check |
| // because these checks should also apply to the image node itself before |
| // breaking. |
| if (UnvisitedNodeOrAncestorHasContextMenuListener( |
| node, already_visited_nodes_for_context_menu_listener)) { |
| outcome |= EnumToBitmask(kFoundContextMenuListener); |
| // Don't break because it allows us to log the failure reason only |
| // if an image node was otherwise available lower in the hit test. |
| } |
| if (top_hit_frame != node->GetDocument().GetFrame()) { |
| outcome |= EnumToBitmask(kBlockedByCrossFrameNode); |
| // Don't break because it allows us to log the failure reason only |
| // if an image node was otherwise available lower in the hit test. |
| } |
| |
| if (IsA<HTMLCanvasElement>(node) || |
| !HitTestResult::AbsoluteImageURL(node).IsEmpty()) { |
| found_image_node = node; |
| |
| if (hit_test_depth == 1) { |
| outcome |= EnumToBitmask(kImageFoundStandard); |
| // The context menu listener check is only necessary when penetrating, |
| // so clear the bit so we don't want to log it if the image was on top. |
| outcome &= ~EnumToBitmask(kFoundContextMenuListener); |
| } else { |
| outcome |= EnumToBitmask(kImageFoundPenetrating); |
| } |
| break; |
| } |
| // IMPORTANT: Check after image checks above so that non-transparent |
| // image elements don't trigger the opaque check. |
| if (node->GetLayoutBox() != nullptr && |
| node->GetLayoutBox()->BackgroundIsKnownToBeOpaqueInRect( |
| HitTestLocation::RectForPoint( |
| hit_test_result_.PointInInnerNodeFrame()))) { |
| outcome |= EnumToBitmask(kBlockedByOpaqueNode); |
| // Don't break because it allows us to log the failure reason only |
| // if an image node was otherwise available lower in the hit test. |
| } |
| } |
| |
| // Only log if we found an image node within the hit test. |
| if (report_histograms && (found_image_node != nullptr)) { |
| base::UmaHistogramCounts1000("Blink.ContextMenu.ImageSelection.Depth", |
| hit_test_depth); |
| for (uint32_t i = 0; i < kMaxValue; i++) { |
| unsigned val = 1 << i; |
| if (outcome & val) { |
| base::UmaHistogramEnumeration( |
| "Blink.ContextMenu.ImageSelection.Outcome", |
| ImageSelectionOutcome(i)); |
| MaybeRecordImageSelectionUkm( |
| found_image_node->GetDocument().UkmSourceID(), |
| ImageSelectionOutcome(i)); |
| } |
| } |
| } |
| // If there is anything preventing this image selection, return nullptr. |
| uint32_t blocking_image_selection_mask = |
| ~(EnumToBitmask(kImageFoundStandard) | |
| EnumToBitmask(kImageFoundPenetrating)); |
| if (outcome & blocking_image_selection_mask) { |
| return nullptr; |
| } |
| return found_image_node; |
| } |
| |
| // TODO(crbug.com/1184297) Cache image node when the context menu is shown and |
| // return that rather than refetching. |
| Node* ContextMenuController::ContextMenuImageNodeForFrame(LocalFrame* frame) { |
| if (base::FeatureList::IsEnabled( |
| features::kEnablePenetratingImageSelection)) { |
| // Don't report histograms because they were already sent for this node when |
| // ContextMenuData was populated. |
| Node* potential_image_node = GetContextMenuNodeWithImageContents( |
| /*report_histograms=*/false); |
| return potential_image_node != nullptr && |
| potential_image_node->GetDocument().GetFrame() == frame |
| ? potential_image_node |
| : nullptr; |
| } |
| return ContextMenuNodeForFrame(frame); |
| } |
| |
| // TODO(crbug.com/1184297) Cache image node when the context menu is shown and |
| // return that rather than refetching. |
| Node* ContextMenuController::ContextMenuNodeForFrame(LocalFrame* frame) { |
| return hit_test_result_.InnerNodeFrame() == frame |
| ? hit_test_result_.InnerNodeOrImageMapImage() |
| : nullptr; |
| } |
| |
| void ContextMenuController::CustomContextMenuAction(uint32_t action) { |
| CustomContextMenuItemSelected(action); |
| } |
| |
| void ContextMenuController::ContextMenuClosed(const KURL& link_followed) { |
| if (!link_followed.IsValid()) |
| return; |
| |
| WebLocalFrameImpl* selected_web_frame = |
| WebLocalFrameImpl::FromFrame(hit_test_result_.InnerNodeFrame()); |
| if (!selected_web_frame) |
| return; |
| |
| selected_web_frame->SendPings(link_followed); |
| } |
| |
| static int ComputeEditFlags(Document& selected_document, Editor& editor) { |
| int edit_flags = ContextMenuDataEditFlags::kCanDoNone; |
| if (editor.CanUndo()) |
| edit_flags |= ContextMenuDataEditFlags::kCanUndo; |
| if (editor.CanRedo()) |
| edit_flags |= ContextMenuDataEditFlags::kCanRedo; |
| if (editor.CanCut()) |
| edit_flags |= ContextMenuDataEditFlags::kCanCut; |
| if (editor.CanCopy()) |
| edit_flags |= ContextMenuDataEditFlags::kCanCopy; |
| if (editor.CanPaste()) |
| edit_flags |= ContextMenuDataEditFlags::kCanPaste; |
| if (editor.CanDelete()) |
| edit_flags |= ContextMenuDataEditFlags::kCanDelete; |
| if (editor.CanEditRichly()) |
| edit_flags |= ContextMenuDataEditFlags::kCanEditRichly; |
| if (IsA<HTMLDocument>(selected_document) || |
| selected_document.IsXHTMLDocument()) { |
| edit_flags |= ContextMenuDataEditFlags::kCanTranslate; |
| if (selected_document.queryCommandEnabled("selectAll", ASSERT_NO_EXCEPTION)) |
| edit_flags |= ContextMenuDataEditFlags::kCanSelectAll; |
| } |
| return edit_flags; |
| } |
| |
| static mojom::blink::ContextMenuDataInputFieldType ComputeInputFieldType( |
| HitTestResult& result) { |
| if (auto* input = DynamicTo<HTMLInputElement>(result.InnerNode())) { |
| if (input->type() == input_type_names::kPassword) |
| return mojom::blink::ContextMenuDataInputFieldType::kPassword; |
| if (input->type() == input_type_names::kNumber) |
| return mojom::blink::ContextMenuDataInputFieldType::kNumber; |
| if (input->type() == input_type_names::kTel) |
| return mojom::blink::ContextMenuDataInputFieldType::kTelephone; |
| if (input->IsTextField()) |
| return mojom::blink::ContextMenuDataInputFieldType::kPlainText; |
| return mojom::blink::ContextMenuDataInputFieldType::kOther; |
| } |
| return mojom::blink::ContextMenuDataInputFieldType::kNone; |
| } |
| |
| static gfx::Rect ComputeSelectionRect(LocalFrame* selected_frame) { |
| IntRect anchor; |
| IntRect focus; |
| selected_frame->Selection().ComputeAbsoluteBounds(anchor, focus); |
| anchor = selected_frame->View()->FrameToViewport(anchor); |
| focus = selected_frame->View()->FrameToViewport(focus); |
| int left = std::min(focus.X(), anchor.X()); |
| int top = std::min(focus.Y(), anchor.Y()); |
| int right = std::max(focus.X() + focus.Width(), anchor.X() + anchor.Width()); |
| int bottom = |
| std::max(focus.Y() + focus.Height(), anchor.Y() + anchor.Height()); |
| // Intersect the selection rect and the visible bounds of the focused_element |
| // to ensure the selection rect is visible. |
| Document* doc = selected_frame->GetDocument(); |
| if (doc) { |
| Element* focused_element = doc->FocusedElement(); |
| if (focused_element) { |
| IntRect visible_bound = focused_element->VisibleBoundsInVisualViewport(); |
| left = std::max(visible_bound.X(), left); |
| top = std::max(visible_bound.Y(), top); |
| right = std::min(visible_bound.MaxX(), right); |
| bottom = std::min(visible_bound.MaxY(), bottom); |
| } |
| } |
| |
| return gfx::Rect(left, top, right - left, bottom - top); |
| } |
| |
| bool ContextMenuController::ShouldShowContextMenuFromTouch( |
| const ContextMenuData& data) { |
| return page_->GetSettings().GetAlwaysShowContextMenuOnTouch() || |
| !data.link_url.is_empty() || |
| data.media_type == mojom::blink::ContextMenuDataMediaType::kImage || |
| data.media_type == mojom::blink::ContextMenuDataMediaType::kVideo || |
| data.is_editable || !data.selected_text.empty(); |
| } |
| |
| bool ContextMenuController::ShowContextMenu(LocalFrame* frame, |
| const PhysicalOffset& point, |
| WebMenuSourceType source_type, |
| const MouseEvent* mouse_event) { |
| // Displaying the context menu in this function is a big hack as we don't |
| // have context, i.e. whether this is being invoked via a script or in |
| // response to user input (Mouse event WM_RBUTTONDOWN, |
| // Keyboard events KeyVK_APPS, Shift+F10). Check if this is being invoked |
| // in response to the above input events before popping up the context menu. |
| if (!ContextMenuAllowedScope::IsContextMenuAllowed()) |
| return false; |
| |
| if (context_menu_client_receiver_.is_bound()) |
| context_menu_client_receiver_.reset(); |
| |
| HitTestRequest::HitTestRequestType type = HitTestRequest::kReadOnly | |
| HitTestRequest::kActive | |
| HitTestRequest::kRetargetForInert; |
| if (base::FeatureList::IsEnabled( |
| features::kEnablePenetratingImageSelection)) { |
| type |= HitTestRequest::kPenetratingList | HitTestRequest::kListBased; |
| } |
| |
| HitTestLocation location(point); |
| HitTestResult result(type, location); |
| if (frame) |
| result = frame->GetEventHandler().HitTestResultAtLocation(location, type); |
| if (!result.InnerNodeOrImageMapImage()) |
| return false; |
| |
| hit_test_result_ = result; |
| result.SetToShadowHostIfInRestrictedShadowRoot(); |
| |
| LocalFrame* selected_frame = result.InnerNodeFrame(); |
| // Tests that do not require selection pass mouse_event = nullptr |
| if (mouse_event) { |
| selected_frame->GetEventHandler() |
| .GetSelectionController() |
| .UpdateSelectionForContextMenuEvent( |
| mouse_event, hit_test_result_, |
| PhysicalOffset(FlooredIntPoint(point))); |
| } |
| |
| ContextMenuData data; |
| data.mouse_position = selected_frame->View()->FrameToViewport( |
| result.RoundedPointInInnerNodeFrame()); |
| |
| data.edit_flags = ComputeEditFlags( |
| *selected_frame->GetDocument(), |
| To<LocalFrame>(page_->GetFocusController().FocusedOrMainFrame()) |
| ->GetEditor()); |
| |
| if (mouse_event && source_type == kMenuSourceKeyboard) { |
| Node* target_node = mouse_event->target()->ToNode(); |
| if (target_node && IsA<Element>(target_node)) { |
| // Get the url from an explicitly set target, e.g. the focused element |
| // when the context menu is evoked from the keyboard. Note: the innerNode |
| // could also be set. It is used to identify a relevant inner media |
| // element. In most cases, the innerNode will already be set to any |
| // relevant inner media element via the median x,y point from the focused |
| // element's bounding box. As the media element in most cases fills the |
| // entire area of a focused link or button, this generally suffices. |
| // Example: When Shift+F10 is used with <a><img></a>, any image-related |
| // context menu options, such as open image in new tab, must be presented. |
| result.SetURLElement(target_node->EnclosingLinkEventParentOrSelf()); |
| } |
| } |
| data.link_url = result.AbsoluteLinkURL(); |
| |
| auto* html_element = DynamicTo<HTMLElement>(result.InnerNode()); |
| if (html_element) { |
| data.title_text = html_element->title().Utf8(); |
| data.alt_text = html_element->AltText().Utf8(); |
| } |
| if (!result.AbsoluteMediaURL().IsEmpty() || |
| result.GetMediaStreamDescriptor()) { |
| if (!result.AbsoluteMediaURL().IsEmpty()) |
| data.src_url = result.AbsoluteMediaURL(); |
| |
| // We know that if absoluteMediaURL() is not empty or element has a media |
| // stream descriptor, then this is a media element. |
| auto* media_element = To<HTMLMediaElement>(result.InnerNode()); |
| if (IsA<HTMLVideoElement>(*media_element)) { |
| // A video element should be presented as an audio element when it has an |
| // audio track but no video track. |
| if (media_element->HasAudio() && !media_element->HasVideo()) |
| data.media_type = mojom::blink::ContextMenuDataMediaType::kAudio; |
| else |
| data.media_type = mojom::blink::ContextMenuDataMediaType::kVideo; |
| if (media_element->SupportsPictureInPicture()) { |
| data.media_flags |= ContextMenuData::kMediaCanPictureInPicture; |
| if (PictureInPictureController::IsElementInPictureInPicture( |
| media_element)) |
| data.media_flags |= ContextMenuData::kMediaPictureInPicture; |
| } |
| } else if (IsA<HTMLAudioElement>(*media_element)) { |
| data.media_type = mojom::blink::ContextMenuDataMediaType::kAudio; |
| } |
| |
| data.suggested_filename = media_element->title().Utf8(); |
| if (media_element->error()) |
| data.media_flags |= ContextMenuData::kMediaInError; |
| if (media_element->paused()) |
| data.media_flags |= ContextMenuData::kMediaPaused; |
| if (media_element->muted()) |
| data.media_flags |= ContextMenuData::kMediaMuted; |
| if (media_element->SupportsLoop()) |
| data.media_flags |= ContextMenuData::kMediaCanLoop; |
| if (media_element->Loop()) |
| data.media_flags |= ContextMenuData::kMediaLoop; |
| if (media_element->SupportsSave()) |
| data.media_flags |= ContextMenuData::kMediaCanSave; |
| if (media_element->HasAudio()) |
| data.media_flags |= ContextMenuData::kMediaHasAudio; |
| // Media controls can be toggled only for video player. If we toggle |
| // controls for audio then the player disappears, and there is no way to |
| // return it back. Don't set this bit for fullscreen video, since |
| // toggling is ignored in that case. |
| if (IsA<HTMLVideoElement>(media_element) && media_element->HasVideo() && |
| !media_element->IsFullscreen()) |
| data.media_flags |= ContextMenuData::kMediaCanToggleControls; |
| if (media_element->ShouldShowControls()) |
| data.media_flags |= ContextMenuData::kMediaControls; |
| } else if (IsA<HTMLObjectElement>(*result.InnerNode()) || |
| IsA<HTMLEmbedElement>(*result.InnerNode())) { |
| if (auto* embedded = DynamicTo<LayoutEmbeddedContent>( |
| result.InnerNode()->GetLayoutObject())) { |
| WebPluginContainerImpl* plugin_view = embedded->Plugin(); |
| if (plugin_view) { |
| data.media_type = mojom::blink::ContextMenuDataMediaType::kPlugin; |
| |
| WebPlugin* plugin = plugin_view->Plugin(); |
| data.link_url = KURL(plugin->LinkAtPosition(data.mouse_position) |
| .GetString() |
| .Utf8() |
| .c_str()); |
| |
| auto* plugin_element = To<HTMLPlugInElement>(result.InnerNode()); |
| data.src_url = |
| plugin_element->GetDocument().CompleteURL(plugin_element->Url()); |
| |
| // Figure out the text selection and text edit flags. |
| WebString text = plugin->SelectionAsText(); |
| if (!text.IsEmpty()) { |
| data.selected_text = text.Utf8(); |
| data.edit_flags |= ContextMenuDataEditFlags::kCanCopy; |
| } |
| bool plugin_can_edit_text = plugin->CanEditText(); |
| if (plugin_can_edit_text) { |
| data.is_editable = true; |
| if (!!(data.edit_flags & ContextMenuDataEditFlags::kCanCopy)) |
| data.edit_flags |= ContextMenuDataEditFlags::kCanCut; |
| data.edit_flags |= ContextMenuDataEditFlags::kCanPaste; |
| |
| if (plugin->HasEditableText()) |
| data.edit_flags |= ContextMenuDataEditFlags::kCanSelectAll; |
| |
| if (plugin->CanUndo()) |
| data.edit_flags |= ContextMenuDataEditFlags::kCanUndo; |
| if (plugin->CanRedo()) |
| data.edit_flags |= ContextMenuDataEditFlags::kCanRedo; |
| } |
| // Disable translation for plugins. |
| data.edit_flags &= ~ContextMenuDataEditFlags::kCanTranslate; |
| |
| // Figure out the media flags. |
| data.media_flags |= ContextMenuData::kMediaCanSave; |
| if (plugin->SupportsPaginatedPrint()) |
| data.media_flags |= ContextMenuData::kMediaCanPrint; |
| |
| // Add context menu commands that are supported by the plugin. |
| // Only show rotate view options if focus is not in an editable text |
| // area. |
| if (!plugin_can_edit_text && plugin->CanRotateView()) |
| data.media_flags |= ContextMenuData::kMediaCanRotate; |
| } |
| } |
| } else { |
| // Check image media last to ensure that penetrating image selection |
| // does not override a topmost media element. |
| // TODO(benwgold): Consider extending penetration to all media types. |
| Node* potential_image_node = result.InnerNodeOrImageMapImage(); |
| if (base::FeatureList::IsEnabled( |
| features::kEnablePenetratingImageSelection)) { |
| SCOPED_BLINK_UMA_HISTOGRAM_TIMER( |
| "Blink.ContextMenu.ImageSelection.ElapsedTime"); |
| potential_image_node = |
| GetContextMenuNodeWithImageContents(/*report_histograms=*/true); |
| } |
| |
| if (potential_image_node != nullptr && |
| IsA<HTMLCanvasElement>(potential_image_node)) { |
| data.media_type = mojom::blink::ContextMenuDataMediaType::kCanvas; |
| data.has_image_contents = true; |
| } else if (potential_image_node != nullptr && |
| !HitTestResult::AbsoluteImageURL(potential_image_node) |
| .IsEmpty()) { |
| data.src_url = HitTestResult::AbsoluteImageURL(potential_image_node); |
| data.media_type = mojom::blink::ContextMenuDataMediaType::kImage; |
| data.media_flags |= ContextMenuData::kMediaCanPrint; |
| data.has_image_contents = |
| HitTestResult::GetImage(potential_image_node) && |
| !HitTestResult::GetImage(potential_image_node)->IsNull(); |
| } |
| } |
| // If it's not a link, an image, a media element, or an image/media link, |
| // show a selection menu or a more generic page menu. |
| if (selected_frame->GetDocument()->Loader()) { |
| data.frame_encoding = |
| selected_frame->GetDocument()->EncodingName().GetString().Utf8(); |
| } |
| |
| data.selection_start_offset = 0; |
| // HitTestResult::isSelected() ensures clean layout by performing a hit test. |
| // If source_type is |kMenuSourceAdjustSelection| or |
| // |kMenuSourceAdjustSelectionReset| we know the original HitTestResult in |
| // SelectionController passed the inside check already, so let it pass. |
| if (result.IsSelected(location) || |
| source_type == kMenuSourceAdjustSelection || |
| source_type == kMenuSourceAdjustSelectionReset) { |
| data.selected_text = selected_frame->SelectedText().Utf8(); |
| WebRange range = |
| selected_frame->GetInputMethodController().GetSelectionOffsets(); |
| data.selection_start_offset = range.StartOffset(); |
| // TODO(crbug.com/850954): Remove redundant log after we identified the |
| // issue. |
| CHECK_GE(data.selection_start_offset, 0) |
| << "Log issue against https://crbug.com/850954\n" |
| << "data.selection_start_offset: " << data.selection_start_offset |
| << "\nrange: [" << range.StartOffset() << ", " << range.EndOffset() |
| << "]\nVisibleSelection: " |
| << selected_frame->Selection() |
| .ComputeVisibleSelectionInDOMTreeDeprecated(); |
| |
| // Store text selection when it happens as it might be cleared when the |
| // browser will request |TextFragmentSelectorGenerator| to generate |
| // selector. |
| UpdateTextFragmentSelectorGenerator(selected_frame); |
| } |
| |
| if (result.IsContentEditable()) { |
| data.is_editable = true; |
| SpellChecker& spell_checker = selected_frame->GetSpellChecker(); |
| |
| // Spellchecker adds spelling markers to misspelled words and attaches |
| // suggestions to these markers in the background. Therefore, when a |
| // user right-clicks a mouse on a word, Chrome just needs to find a |
| // spelling marker on the word instead of spellchecking it. |
| std::pair<String, String> misspelled_word_and_description = |
| spell_checker.SelectMisspellingAsync(); |
| data.misspelled_word = |
| WebString::FromUTF8(misspelled_word_and_description.first.Utf8()) |
| .Utf16(); |
| const String& description = misspelled_word_and_description.second; |
| if (description.length()) { |
| Vector<String> suggestions; |
| description.Split('\n', suggestions); |
| WebVector<base::string16> web_suggestions(suggestions.size()); |
| std::transform(suggestions.begin(), suggestions.end(), |
| web_suggestions.begin(), [](const String& s) { |
| return WebString::FromUTF8(s.Utf8()).Utf16(); |
| }); |
| data.dictionary_suggestions = web_suggestions.ReleaseVector(); |
| } else if (spell_checker.GetTextCheckerClient()) { |
| size_t misspelled_offset, misspelled_length; |
| WebVector<WebString> web_suggestions; |
| spell_checker.GetTextCheckerClient()->CheckSpelling( |
| WebString::FromUTF16(data.misspelled_word), misspelled_offset, |
| misspelled_length, &web_suggestions); |
| WebVector<base::string16> suggestions(web_suggestions.size()); |
| std::transform(web_suggestions.begin(), web_suggestions.end(), |
| suggestions.begin(), |
| [](const WebString& s) { return s.Utf16(); }); |
| data.dictionary_suggestions = suggestions.ReleaseVector(); |
| } |
| } |
| |
| if (EditingStyle::SelectionHasStyle(*selected_frame, |
| CSSPropertyID::kDirection, |
| "ltr") != EditingTriState::kFalse) { |
| data.writing_direction_left_to_right |= |
| ContextMenuData::kCheckableMenuItemChecked; |
| } |
| if (EditingStyle::SelectionHasStyle(*selected_frame, |
| CSSPropertyID::kDirection, |
| "rtl") != EditingTriState::kFalse) { |
| data.writing_direction_right_to_left |= |
| ContextMenuData::kCheckableMenuItemChecked; |
| } |
| |
| data.referrer_policy = selected_frame->DomWindow()->GetReferrerPolicy(); |
| |
| if (menu_provider_) { |
| // Filter out custom menu elements and add them into the data. |
| data.custom_items = menu_provider_->PopulateContextMenu().ReleaseVector(); |
| } |
| |
| if (auto* anchor = DynamicTo<HTMLAnchorElement>(result.URLElement())) { |
| // Extract suggested filename for same-origin URLS for saving file. |
| const SecurityOrigin* origin = |
| selected_frame->GetSecurityContext()->GetSecurityOrigin(); |
| if (origin->CanReadContent(anchor->Url())) { |
| data.suggested_filename = |
| anchor->FastGetAttribute(html_names::kDownloadAttr).Utf8(); |
| } |
| |
| // If the anchor wants to suppress the referrer, update the referrerPolicy |
| // accordingly. |
| if (anchor->HasRel(kRelationNoReferrer)) |
| data.referrer_policy = network::mojom::ReferrerPolicy::kNever; |
| |
| data.link_text = anchor->innerText().Utf8(); |
| |
| if (anchor->HasImpression()) { |
| base::Optional<WebImpression> web_impression = |
| GetImpressionForAnchor(anchor); |
| data.impression = |
| web_impression.has_value() |
| ? base::Optional<Impression>( |
| ConvertWebImpressionToImpression(web_impression.value())) |
| : base::nullopt; |
| } |
| } |
| |
| data.input_field_type = ComputeInputFieldType(result); |
| data.selection_rect = ComputeSelectionRect(selected_frame); |
| data.source_type = source_type; |
| |
| const bool from_touch = source_type == kMenuSourceTouch || |
| source_type == kMenuSourceLongPress || |
| source_type == kMenuSourceLongTap; |
| if (from_touch && !ShouldShowContextMenuFromTouch(data)) |
| return false; |
| |
| base::Optional<gfx::Point> host_context_menu_location; |
| auto* main_frame = |
| WebLocalFrameImpl::FromFrame(DynamicTo<LocalFrame>(page_->MainFrame())); |
| if (main_frame) { |
| host_context_menu_location = |
| main_frame->FrameWidgetImpl()->GetAndResetContextMenuLocation(); |
| } |
| |
| WebLocalFrameImpl* selected_web_frame = |
| WebLocalFrameImpl::FromFrame(selected_frame); |
| if (!selected_web_frame || !selected_web_frame->Client()) |
| return false; |
| |
| selected_web_frame->ShowContextMenu( |
| context_menu_client_receiver_.BindNewEndpointAndPassRemote( |
| selected_web_frame->GetTaskRunner(TaskType::kInternalDefault)), |
| data, host_context_menu_location); |
| |
| return true; |
| } |
| |
| void ContextMenuController::UpdateTextFragmentSelectorGenerator( |
| LocalFrame* selected_frame) { |
| if (!selected_frame->GetTextFragmentSelectorGenerator()) |
| return; |
| |
| VisibleSelectionInFlatTree selection = |
| selected_frame->Selection().ComputeVisibleSelectionInFlatTree(); |
| EphemeralRangeInFlatTree selection_range(selection.Start(), selection.End()); |
| selected_frame->GetTextFragmentSelectorGenerator()->UpdateSelection( |
| selected_frame, selection_range); |
| } |
| |
| } // namespace blink |