blob: aa0437bc2b0b254a31ae1e9b2d28bcb87a4def7e [file] [log] [blame]
/*
* Copyright (C) 2009, 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "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 THE COPYRIGHT
* OWNER 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 "web/ContextMenuClientImpl.h"
#include "bindings/core/v8/ExceptionState.h"
#include "core/CSSPropertyNames.h"
#include "core/HTMLNames.h"
#include "core/InputTypeNames.h"
#include "core/css/CSSStyleDeclaration.h"
#include "core/dom/Document.h"
#include "core/dom/ElementTraversal.h"
#include "core/editing/Editor.h"
#include "core/editing/markers/DocumentMarkerController.h"
#include "core/editing/spellcheck/SpellChecker.h"
#include "core/exported/WebDataSourceImpl.h"
#include "core/exported/WebPluginContainerBase.h"
#include "core/exported/WebViewBase.h"
#include "core/frame/FrameView.h"
#include "core/frame/Settings.h"
#include "core/frame/VisualViewport.h"
#include "core/frame/WebLocalFrameBase.h"
#include "core/html/HTMLAnchorElement.h"
#include "core/html/HTMLFormElement.h"
#include "core/html/HTMLFrameElementBase.h"
#include "core/html/HTMLImageElement.h"
#include "core/html/HTMLInputElement.h"
#include "core/html/HTMLMediaElement.h"
#include "core/html/HTMLPlugInElement.h"
#include "core/input/ContextMenuAllowedScope.h"
#include "core/input/EventHandler.h"
#include "core/layout/HitTestResult.h"
#include "core/layout/LayoutPart.h"
#include "core/loader/DocumentLoader.h"
#include "core/loader/FrameLoader.h"
#include "core/loader/HistoryItem.h"
#include "core/page/ContextMenuController.h"
#include "core/page/Page.h"
#include "platform/ContextMenu.h"
#include "platform/exported/WrappedResourceResponse.h"
#include "platform/text/TextBreakIterator.h"
#include "platform/weborigin/KURL.h"
#include "platform/wtf/text/WTFString.h"
#include "public/platform/WebPoint.h"
#include "public/platform/WebString.h"
#include "public/platform/WebURL.h"
#include "public/platform/WebURLResponse.h"
#include "public/platform/WebVector.h"
#include "public/web/WebContextMenuData.h"
#include "public/web/WebFormElement.h"
#include "public/web/WebFrameClient.h"
#include "public/web/WebMenuItemInfo.h"
#include "public/web/WebPlugin.h"
#include "public/web/WebSearchableFormData.h"
#include "public/web/WebTextCheckClient.h"
#include "public/web/WebViewClient.h"
namespace blink {
// Figure out the URL of a page or subframe. Returns |page_type| as the type,
// which indicates page or subframe, or ContextNodeType::kNone if the URL could
// not be determined for some reason.
static WebURL UrlFromFrame(LocalFrame* frame) {
if (frame) {
DocumentLoader* dl = frame->Loader().GetDocumentLoader();
if (dl) {
WebDataSource* ds = WebDataSourceImpl::FromDocumentLoader(dl);
if (ds) {
return ds->HasUnreachableURL() ? ds->UnreachableURL()
: ds->GetRequest().Url();
}
}
}
return WebURL();
}
static bool IsWhiteSpaceOrPunctuation(UChar c) {
return IsSpaceOrNewline(c) || WTF::Unicode::IsPunct(c);
}
static String SelectMisspellingAsync(LocalFrame* selected_frame,
String& description) {
VisibleSelection selection =
selected_frame->Selection().ComputeVisibleSelectionInDOMTree();
if (selection.IsNone())
return String();
// Caret and range selections always return valid normalized ranges.
const EphemeralRange& selection_range =
selection.ToNormalizedEphemeralRange();
Node* const selection_start_container =
selection_range.StartPosition().ComputeContainerNode();
Node* const selection_end_container =
selection_range.EndPosition().ComputeContainerNode();
// We don't currently support the case where a misspelling spans multiple
// nodes
if (selection_start_container != selection_end_container)
return String();
const unsigned selection_start_offset =
selection_range.StartPosition().ComputeOffsetInContainerNode();
const unsigned selection_end_offset =
selection_range.EndPosition().ComputeOffsetInContainerNode();
const DocumentMarkerVector& markers_in_node =
selected_frame->GetDocument()->Markers().MarkersFor(
selection_start_container, DocumentMarker::MisspellingMarkers());
const auto marker_it =
std::find_if(markers_in_node.begin(), markers_in_node.end(),
[=](const DocumentMarker* marker) {
return marker->StartOffset() < selection_end_offset &&
marker->EndOffset() > selection_start_offset;
});
if (marker_it == markers_in_node.end())
return String();
const DocumentMarker* const found_marker = *marker_it;
description = found_marker->Description();
Range* const marker_range =
Range::Create(*selected_frame->GetDocument(), selection_start_container,
found_marker->StartOffset(), selection_start_container,
found_marker->EndOffset());
if (marker_range->GetText().StripWhiteSpace(&IsWhiteSpaceOrPunctuation) !=
CreateRange(selection_range)
->GetText()
.StripWhiteSpace(&IsWhiteSpaceOrPunctuation))
return String();
return marker_range->GetText();
}
// static
int ContextMenuClientImpl::ComputeEditFlags(Document& selected_document,
Editor& editor) {
int edit_flags = WebContextMenuData::kCanDoNone;
if (!selected_document.IsHTMLDocument() &&
!selected_document.IsXHTMLDocument())
return edit_flags;
edit_flags |= WebContextMenuData::kCanTranslate;
if (editor.CanUndo())
edit_flags |= WebContextMenuData::kCanUndo;
if (editor.CanRedo())
edit_flags |= WebContextMenuData::kCanRedo;
if (editor.CanCut())
edit_flags |= WebContextMenuData::kCanCut;
if (editor.CanCopy())
edit_flags |= WebContextMenuData::kCanCopy;
if (editor.CanPaste())
edit_flags |= WebContextMenuData::kCanPaste;
if (editor.CanDelete())
edit_flags |= WebContextMenuData::kCanDelete;
if (editor.CanEditRichly())
edit_flags |= WebContextMenuData::kCanEditRichly;
if (selected_document.queryCommandEnabled("selectAll", ASSERT_NO_EXCEPTION))
edit_flags |= WebContextMenuData::kCanSelectAll;
return edit_flags;
}
bool ContextMenuClientImpl::ShouldShowContextMenuFromTouch(
const WebContextMenuData& data) {
return web_view_->GetPage()
->GetSettings()
.GetAlwaysShowContextMenuOnTouch() ||
!data.link_url.IsEmpty() ||
data.media_type == WebContextMenuData::kMediaTypeImage ||
data.media_type == WebContextMenuData::kMediaTypeVideo ||
data.is_editable;
}
static HTMLFormElement* AssociatedFormElement(HTMLElement& element) {
if (isHTMLFormElement(element))
return &toHTMLFormElement(element);
return element.formOwner();
}
// Scans logically forward from "start", including any child frames.
static HTMLFormElement* ScanForForm(const Node* start) {
if (!start)
return nullptr;
for (HTMLElement& element : Traversal<HTMLElement>::StartsAt(
start->IsHTMLElement() ? ToHTMLElement(start)
: Traversal<HTMLElement>::Next(*start))) {
if (HTMLFormElement* form = AssociatedFormElement(element))
return form;
if (IsHTMLFrameElementBase(element)) {
Node* child_document = ToHTMLFrameElementBase(element).contentDocument();
if (HTMLFormElement* frame_result = ScanForForm(child_document))
return frame_result;
}
}
return nullptr;
}
// We look for either the form containing the current focus, or for one
// immediately after it
static HTMLFormElement* CurrentForm(const FrameSelection& current_selection) {
// Start looking either at the active (first responder) node, or where the
// selection is.
const Node* start = current_selection.GetDocument().FocusedElement();
if (!start) {
start = current_selection.ComputeVisibleSelectionInDOMTree()
.Start()
.AnchorNode();
}
if (!start)
return nullptr;
// Try walking up the node tree to find a form element.
for (Node& node : NodeTraversal::InclusiveAncestorsOf(*start)) {
if (!node.IsHTMLElement())
break;
HTMLElement& element = ToHTMLElement(node);
if (HTMLFormElement* form = AssociatedFormElement(element))
return form;
}
// Try walking forward in the node tree to find a form element.
return ScanForForm(start);
}
bool ContextMenuClientImpl::ShowContextMenu(const ContextMenu* default_menu,
bool from_touch) {
// 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;
HitTestResult r =
web_view_->GetPage()->GetContextMenuController().GetHitTestResult();
r.SetToShadowHostIfInRestrictedShadowRoot();
LocalFrame* selected_frame = r.InnerNodeFrame();
WebLocalFrameBase* selected_web_frame =
WebLocalFrameBase::FromFrame(selected_frame);
WebContextMenuData data;
data.mouse_position = selected_frame->View()->ContentsToViewport(
r.RoundedPointInInnerNodeFrame());
data.edit_flags = ComputeEditFlags(
*selected_frame->GetDocument(),
ToLocalFrame(web_view_->FocusedCoreFrame())->GetEditor());
// Links, Images, Media tags, and Image/Media-Links take preference over
// all else.
data.link_url = r.AbsoluteLinkURL();
if (r.InnerNode()->IsHTMLElement()) {
HTMLElement* html_element = ToHTMLElement(r.InnerNode());
if (!html_element->title().IsEmpty()) {
data.title_text = html_element->title();
} else {
data.title_text = html_element->AltText();
}
}
if (isHTMLCanvasElement(r.InnerNode())) {
data.media_type = WebContextMenuData::kMediaTypeCanvas;
data.has_image_contents = true;
} else if (!r.AbsoluteImageURL().IsEmpty()) {
data.src_url = r.AbsoluteImageURL();
data.media_type = WebContextMenuData::kMediaTypeImage;
data.media_flags |= WebContextMenuData::kMediaCanPrint;
// An image can be null for many reasons, like being blocked, no image
// data received from server yet.
data.has_image_contents = r.GetImage() && !r.GetImage()->IsNull();
if (data.has_image_contents &&
isHTMLImageElement(r.InnerNodeOrImageMapImage())) {
HTMLImageElement* image_element =
toHTMLImageElement(r.InnerNodeOrImageMapImage());
if (image_element && image_element->CachedImage())
data.image_response = WrappedResourceResponse(
image_element->CachedImage()->GetResponse());
}
} else if (!r.AbsoluteMediaURL().IsEmpty()) {
data.src_url = r.AbsoluteMediaURL();
// We know that if absoluteMediaURL() is not empty, then this
// is a media element.
HTMLMediaElement* media_element = ToHTMLMediaElement(r.InnerNode());
if (isHTMLVideoElement(*media_element))
data.media_type = WebContextMenuData::kMediaTypeVideo;
else if (isHTMLAudioElement(*media_element))
data.media_type = WebContextMenuData::kMediaTypeAudio;
if (media_element->error())
data.media_flags |= WebContextMenuData::kMediaInError;
if (media_element->paused())
data.media_flags |= WebContextMenuData::kMediaPaused;
if (media_element->muted())
data.media_flags |= WebContextMenuData::kMediaMuted;
if (media_element->Loop())
data.media_flags |= WebContextMenuData::kMediaLoop;
if (media_element->SupportsSave())
data.media_flags |= WebContextMenuData::kMediaCanSave;
if (media_element->HasAudio())
data.media_flags |= WebContextMenuData::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 (media_element->IsHTMLVideoElement() && media_element->HasVideo() &&
!media_element->IsFullscreen())
data.media_flags |= WebContextMenuData::kMediaCanToggleControls;
if (media_element->ShouldShowControls())
data.media_flags |= WebContextMenuData::kMediaControls;
} else if (isHTMLObjectElement(*r.InnerNode()) ||
isHTMLEmbedElement(*r.InnerNode())) {
LayoutObject* object = r.InnerNode()->GetLayoutObject();
if (object && object->IsLayoutPart()) {
PluginView* plugin_view = ToLayoutPart(object)->Plugin();
if (plugin_view && plugin_view->IsPluginContainer()) {
data.media_type = WebContextMenuData::kMediaTypePlugin;
WebPluginContainerBase* plugin = ToWebPluginContainerBase(plugin_view);
WebString text = plugin->Plugin()->SelectionAsText();
if (!text.IsEmpty()) {
data.selected_text = text;
data.edit_flags |= WebContextMenuData::kCanCopy;
}
data.edit_flags &= ~WebContextMenuData::kCanTranslate;
data.link_url = plugin->Plugin()->LinkAtPosition(data.mouse_position);
if (plugin->Plugin()->SupportsPaginatedPrint())
data.media_flags |= WebContextMenuData::kMediaCanPrint;
HTMLPlugInElement* plugin_element = ToHTMLPlugInElement(r.InnerNode());
data.src_url =
plugin_element->GetDocument().CompleteURL(plugin_element->Url());
data.media_flags |= WebContextMenuData::kMediaCanSave;
// Add context menu commands that are supported by the plugin.
if (plugin->Plugin()->CanRotateView())
data.media_flags |= WebContextMenuData::kMediaCanRotate;
}
}
}
// 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();
// Send the frame and page URLs in any case.
if (!web_view_->GetPage()->MainFrame()->IsLocalFrame()) {
// TODO(kenrb): This works around the problem of URLs not being
// available for top-level frames that are in a different process.
// It mostly works to convert the security origin to a URL, but
// extensions accessing that property will not get the correct value
// in that case. See https://crbug.com/534561
WebSecurityOrigin origin = web_view_->MainFrame()->GetSecurityOrigin();
if (!origin.IsNull())
data.page_url = KURL(kParsedURLString, origin.ToString());
} else {
data.page_url =
UrlFromFrame(ToLocalFrame(web_view_->GetPage()->MainFrame()));
}
if (selected_frame != web_view_->GetPage()->MainFrame()) {
data.frame_url = UrlFromFrame(selected_frame);
HistoryItem* history_item =
selected_frame->Loader().GetDocumentLoader()->GetHistoryItem();
if (history_item)
data.frame_history_item = WebHistoryItem(history_item);
}
// HitTestResult::isSelected() ensures clean layout by performing a hit test.
if (r.IsSelected()) {
if (!isHTMLInputElement(*r.InnerNode()) ||
toHTMLInputElement(r.InnerNode())->type() != InputTypeNames::password) {
data.selected_text = selected_frame->SelectedText();
}
}
if (r.IsContentEditable()) {
data.is_editable = true;
// 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.
String description;
data.misspelled_word = SelectMisspellingAsync(selected_frame, description);
if (description.length()) {
Vector<String> suggestions;
description.Split('\n', suggestions);
data.dictionary_suggestions = suggestions;
} else if (selected_web_frame->TextCheckClient()) {
int misspelled_offset, misspelled_length;
selected_web_frame->TextCheckClient()->CheckSpelling(
data.misspelled_word, misspelled_offset, misspelled_length,
&data.dictionary_suggestions);
}
HTMLFormElement* form = CurrentForm(selected_frame->Selection());
if (form && isHTMLInputElement(*r.InnerNode())) {
HTMLInputElement& selected_element = toHTMLInputElement(*r.InnerNode());
WebSearchableFormData ws = WebSearchableFormData(
WebFormElement(form), WebInputElement(&selected_element));
if (ws.Url().IsValid())
data.keyword_url = ws.Url();
}
}
if (selected_frame->GetEditor().SelectionHasStyle(CSSPropertyDirection,
"ltr") != kFalseTriState)
data.writing_direction_left_to_right |=
WebContextMenuData::kCheckableMenuItemChecked;
if (selected_frame->GetEditor().SelectionHasStyle(CSSPropertyDirection,
"rtl") != kFalseTriState)
data.writing_direction_right_to_left |=
WebContextMenuData::kCheckableMenuItemChecked;
data.referrer_policy = static_cast<WebReferrerPolicy>(
selected_frame->GetDocument()->GetReferrerPolicy());
// Filter out custom menu elements and add them into the data.
PopulateCustomMenuItems(default_menu, &data);
if (isHTMLAnchorElement(r.URLElement())) {
HTMLAnchorElement* anchor = toHTMLAnchorElement(r.URLElement());
// Extract suggested filename for saving file.
data.suggested_filename = anchor->FastGetAttribute(HTMLNames::downloadAttr);
// If the anchor wants to suppress the referrer, update the referrerPolicy
// accordingly.
if (anchor->HasRel(kRelationNoReferrer))
data.referrer_policy = kWebReferrerPolicyNever;
data.link_text = anchor->innerText();
}
// Find the input field type.
if (isHTMLInputElement(r.InnerNode())) {
HTMLInputElement* element = toHTMLInputElement(r.InnerNode());
if (element->type() == InputTypeNames::password)
data.input_field_type = WebContextMenuData::kInputFieldTypePassword;
else if (element->IsTextField())
data.input_field_type = WebContextMenuData::kInputFieldTypePlainText;
else
data.input_field_type = WebContextMenuData::kInputFieldTypeOther;
} else {
data.input_field_type = WebContextMenuData::kInputFieldTypeNone;
}
WebRect focus_webrect;
WebRect anchor_webrect;
web_view_->SelectionBounds(focus_webrect, anchor_webrect);
int left = std::min(focus_webrect.x, anchor_webrect.x);
int top = std::min(focus_webrect.y, anchor_webrect.y);
int right = std::max(focus_webrect.x + focus_webrect.width,
anchor_webrect.x + anchor_webrect.width);
int bottom = std::max(focus_webrect.y + focus_webrect.height,
anchor_webrect.y + anchor_webrect.height);
data.selection_rect = WebRect(left, top, right - left, bottom - top);
if (from_touch && !ShouldShowContextMenuFromTouch(data))
return false;
selected_web_frame->SetContextMenuNode(r.InnerNodeOrImageMapImage());
if (!selected_web_frame->Client())
return false;
selected_web_frame->Client()->ShowContextMenu(data);
return true;
}
void ContextMenuClientImpl::ClearContextMenu() {
HitTestResult r =
web_view_->GetPage()->GetContextMenuController().GetHitTestResult();
LocalFrame* selected_frame = r.InnerNodeFrame();
if (!selected_frame)
return;
WebLocalFrameBase* selected_web_frame =
WebLocalFrameBase::FromFrame(selected_frame);
selected_web_frame->ClearContextMenuNode();
}
static void PopulateSubMenuItems(const Vector<ContextMenuItem>& input_menu,
WebVector<WebMenuItemInfo>& sub_menu_items) {
Vector<WebMenuItemInfo> sub_items;
for (size_t i = 0; i < input_menu.size(); ++i) {
const ContextMenuItem* input_item = &input_menu.at(i);
if (input_item->Action() < kContextMenuItemBaseCustomTag ||
input_item->Action() > kContextMenuItemLastCustomTag)
continue;
WebMenuItemInfo output_item;
output_item.label = input_item->Title();
output_item.icon = input_item->Icon();
output_item.enabled = input_item->Enabled();
output_item.checked = input_item->Checked();
output_item.action = static_cast<unsigned>(input_item->Action() -
kContextMenuItemBaseCustomTag);
switch (input_item->GetType()) {
case kActionType:
output_item.type = WebMenuItemInfo::kOption;
break;
case kCheckableActionType:
output_item.type = WebMenuItemInfo::kCheckableOption;
break;
case kSeparatorType:
output_item.type = WebMenuItemInfo::kSeparator;
break;
case kSubmenuType:
output_item.type = WebMenuItemInfo::kSubMenu;
PopulateSubMenuItems(input_item->SubMenuItems(),
output_item.sub_menu_items);
break;
}
sub_items.push_back(output_item);
}
WebVector<WebMenuItemInfo> output_items(sub_items.size());
for (size_t i = 0; i < sub_items.size(); ++i)
output_items[i] = sub_items[i];
sub_menu_items.Swap(output_items);
}
void ContextMenuClientImpl::PopulateCustomMenuItems(
const ContextMenu* default_menu,
WebContextMenuData* data) {
PopulateSubMenuItems(default_menu->Items(), data->custom_items);
}
} // namespace blink