1/* 2 * Copyright (C) 2009, 2012 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31#include "config.h" 32#include "web/ContextMenuClientImpl.h" 33 34#include "bindings/v8/ExceptionStatePlaceholder.h" 35#include "core/CSSPropertyNames.h" 36#include "core/HTMLNames.h" 37#include "core/css/CSSStyleDeclaration.h" 38#include "core/dom/Document.h" 39#include "core/dom/DocumentMarkerController.h" 40#include "core/editing/Editor.h" 41#include "core/editing/SpellChecker.h" 42#include "core/frame/FrameHost.h" 43#include "core/frame/FrameView.h" 44#include "core/frame/PinchViewport.h" 45#include "core/frame/Settings.h" 46#include "core/html/HTMLFormElement.h" 47#include "core/html/HTMLInputElement.h" 48#include "core/html/HTMLMediaElement.h" 49#include "core/html/HTMLPlugInElement.h" 50#include "core/html/MediaError.h" 51#include "core/loader/DocumentLoader.h" 52#include "core/loader/FrameLoader.h" 53#include "core/loader/HistoryItem.h" 54#include "core/page/ContextMenuController.h" 55#include "core/page/EventHandler.h" 56#include "core/page/Page.h" 57#include "core/rendering/HitTestResult.h" 58#include "core/rendering/RenderWidget.h" 59#include "platform/ContextMenu.h" 60#include "platform/Widget.h" 61#include "platform/text/TextBreakIterator.h" 62#include "platform/weborigin/KURL.h" 63#include "public/platform/WebPoint.h" 64#include "public/platform/WebString.h" 65#include "public/platform/WebURL.h" 66#include "public/platform/WebURLResponse.h" 67#include "public/platform/WebVector.h" 68#include "public/web/WebContextMenuData.h" 69#include "public/web/WebFormElement.h" 70#include "public/web/WebFrameClient.h" 71#include "public/web/WebMenuItemInfo.h" 72#include "public/web/WebPlugin.h" 73#include "public/web/WebSearchableFormData.h" 74#include "public/web/WebSpellCheckClient.h" 75#include "public/web/WebViewClient.h" 76#include "web/WebDataSourceImpl.h" 77#include "web/WebLocalFrameImpl.h" 78#include "web/WebPluginContainerImpl.h" 79#include "web/WebViewImpl.h" 80#include "wtf/text/WTFString.h" 81 82using namespace WebCore; 83 84namespace blink { 85 86// Figure out the URL of a page or subframe. Returns |page_type| as the type, 87// which indicates page or subframe, or ContextNodeType::NONE if the URL could not 88// be determined for some reason. 89static WebURL urlFromFrame(LocalFrame* frame) 90{ 91 if (frame) { 92 DocumentLoader* dl = frame->loader().documentLoader(); 93 if (dl) { 94 WebDataSource* ds = WebDataSourceImpl::fromDocumentLoader(dl); 95 if (ds) 96 return ds->hasUnreachableURL() ? ds->unreachableURL() : ds->request().url(); 97 } 98 } 99 return WebURL(); 100} 101 102// Helper function to determine whether text is a single word. 103static bool isASingleWord(const String& text) 104{ 105 TextBreakIterator* it = wordBreakIterator(text, 0, text.length()); 106 return it && it->next() == static_cast<int>(text.length()); 107} 108 109// Helper function to get misspelled word on which context menu 110// is to be invoked. This function also sets the word on which context menu 111// has been invoked to be the selected word, as required. This function changes 112// the selection only when there were no selected characters on OS X. 113static String selectMisspelledWord(LocalFrame* selectedFrame) 114{ 115 // First select from selectedText to check for multiple word selection. 116 String misspelledWord = selectedFrame->selectedText().stripWhiteSpace(); 117 118 // If some texts were already selected, we don't change the selection. 119 if (!misspelledWord.isEmpty()) { 120 // Don't provide suggestions for multiple words. 121 if (!isASingleWord(misspelledWord)) 122 return String(); 123 return misspelledWord; 124 } 125 126 // Selection is empty, so change the selection to the word under the cursor. 127 HitTestResult hitTestResult = selectedFrame->eventHandler(). 128 hitTestResultAtPoint(selectedFrame->page()->contextMenuController().hitTestResult().pointInInnerNodeFrame()); 129 Node* innerNode = hitTestResult.innerNode(); 130 VisiblePosition pos(innerNode->renderer()->positionForPoint( 131 hitTestResult.localPoint())); 132 133 if (pos.isNull()) 134 return misspelledWord; // It is empty. 135 136 WebLocalFrameImpl::selectWordAroundPosition(selectedFrame, pos); 137 misspelledWord = selectedFrame->selectedText().stripWhiteSpace(); 138 139#if OS(MACOSX) 140 // If misspelled word is still empty, then that portion should not be 141 // selected. Set the selection to that position only, and do not expand. 142 if (misspelledWord.isEmpty()) 143 selectedFrame->selection().setSelection(VisibleSelection(pos)); 144#else 145 // On non-Mac, right-click should not make a range selection in any case. 146 selectedFrame->selection().setSelection(VisibleSelection(pos)); 147#endif 148 return misspelledWord; 149} 150 151static bool IsWhiteSpaceOrPunctuation(UChar c) 152{ 153 return isSpaceOrNewline(c) || WTF::Unicode::isPunct(c); 154} 155 156static String selectMisspellingAsync(LocalFrame* selectedFrame, DocumentMarker& marker) 157{ 158 VisibleSelection selection = selectedFrame->selection().selection(); 159 if (!selection.isCaretOrRange()) 160 return String(); 161 162 // Caret and range selections always return valid normalized ranges. 163 RefPtrWillBeRawPtr<Range> selectionRange = selection.toNormalizedRange(); 164 WillBeHeapVector<DocumentMarker*> markers = selectedFrame->document()->markers().markersInRange(selectionRange.get(), DocumentMarker::MisspellingMarkers()); 165 if (markers.size() != 1) 166 return String(); 167 marker = *markers[0]; 168 169 // Cloning a range fails only for invalid ranges. 170 RefPtrWillBeRawPtr<Range> markerRange = selectionRange->cloneRange(); 171 markerRange->setStart(markerRange->startContainer(), marker.startOffset()); 172 markerRange->setEnd(markerRange->endContainer(), marker.endOffset()); 173 174 if (markerRange->text().stripWhiteSpace(&IsWhiteSpaceOrPunctuation) != selectionRange->text().stripWhiteSpace(&IsWhiteSpaceOrPunctuation)) 175 return String(); 176 177 return markerRange->text(); 178} 179 180void ContextMenuClientImpl::showContextMenu(const WebCore::ContextMenu* defaultMenu) 181{ 182 // Displaying the context menu in this function is a big hack as we don't 183 // have context, i.e. whether this is being invoked via a script or in 184 // response to user input (Mouse event WM_RBUTTONDOWN, 185 // Keyboard events KeyVK_APPS, Shift+F10). Check if this is being invoked 186 // in response to the above input events before popping up the context menu. 187 if (!m_webView->contextMenuAllowed()) 188 return; 189 190 HitTestResult r = m_webView->page()->contextMenuController().hitTestResult(); 191 LocalFrame* selectedFrame = r.innerNodeFrame(); 192 193 WebContextMenuData data; 194 IntPoint mousePoint = selectedFrame->view()->contentsToWindow(r.roundedPointInInnerNodeFrame()); 195 196 // FIXME(bokan): crbug.com/371902 - We shouldn't be making these scale 197 // related coordinate transformatios in an ad hoc way. 198 PinchViewport& pinchViewport = selectedFrame->host()->pinchViewport(); 199 mousePoint -= flooredIntSize(pinchViewport.visibleRect().location()); 200 mousePoint.scale(m_webView->pageScaleFactor(), m_webView->pageScaleFactor()); 201 data.mousePosition = mousePoint; 202 203 // Compute edit flags. 204 data.editFlags = WebContextMenuData::CanDoNone; 205 if (toLocalFrame(m_webView->focusedWebCoreFrame())->editor().canUndo()) 206 data.editFlags |= WebContextMenuData::CanUndo; 207 if (toLocalFrame(m_webView->focusedWebCoreFrame())->editor().canRedo()) 208 data.editFlags |= WebContextMenuData::CanRedo; 209 if (toLocalFrame(m_webView->focusedWebCoreFrame())->editor().canCut()) 210 data.editFlags |= WebContextMenuData::CanCut; 211 if (toLocalFrame(m_webView->focusedWebCoreFrame())->editor().canCopy()) 212 data.editFlags |= WebContextMenuData::CanCopy; 213 if (toLocalFrame(m_webView->focusedWebCoreFrame())->editor().canPaste()) 214 data.editFlags |= WebContextMenuData::CanPaste; 215 if (toLocalFrame(m_webView->focusedWebCoreFrame())->editor().canDelete()) 216 data.editFlags |= WebContextMenuData::CanDelete; 217 // We can always select all... 218 data.editFlags |= WebContextMenuData::CanSelectAll; 219 data.editFlags |= WebContextMenuData::CanTranslate; 220 221 // Links, Images, Media tags, and Image/Media-Links take preference over 222 // all else. 223 data.linkURL = r.absoluteLinkURL(); 224 225 if (isHTMLCanvasElement(r.innerNonSharedNode())) { 226 data.mediaType = WebContextMenuData::MediaTypeCanvas; 227 } else if (!r.absoluteImageURL().isEmpty()) { 228 data.srcURL = r.absoluteImageURL(); 229 data.mediaType = WebContextMenuData::MediaTypeImage; 230 data.mediaFlags |= WebContextMenuData::MediaCanPrint; 231 } else if (!r.absoluteMediaURL().isEmpty()) { 232 data.srcURL = r.absoluteMediaURL(); 233 234 // We know that if absoluteMediaURL() is not empty, then this 235 // is a media element. 236 HTMLMediaElement* mediaElement = toHTMLMediaElement(r.innerNonSharedNode()); 237 if (isHTMLVideoElement(*mediaElement)) 238 data.mediaType = WebContextMenuData::MediaTypeVideo; 239 else if (isHTMLAudioElement(*mediaElement)) 240 data.mediaType = WebContextMenuData::MediaTypeAudio; 241 242 if (mediaElement->error()) 243 data.mediaFlags |= WebContextMenuData::MediaInError; 244 if (mediaElement->paused()) 245 data.mediaFlags |= WebContextMenuData::MediaPaused; 246 if (mediaElement->muted()) 247 data.mediaFlags |= WebContextMenuData::MediaMuted; 248 if (mediaElement->loop()) 249 data.mediaFlags |= WebContextMenuData::MediaLoop; 250 if (mediaElement->supportsSave()) 251 data.mediaFlags |= WebContextMenuData::MediaCanSave; 252 if (mediaElement->hasAudio()) 253 data.mediaFlags |= WebContextMenuData::MediaHasAudio; 254 // Media controls can be toggled only for video player. If we toggle 255 // controls for audio then the player disappears, and there is no way to 256 // return it back. Don't set this bit for fullscreen video, since 257 // toggling is ignored in that case. 258 if (mediaElement->hasVideo() && !mediaElement->isFullscreen()) 259 data.mediaFlags |= WebContextMenuData::MediaCanToggleControls; 260 if (mediaElement->controls()) 261 data.mediaFlags |= WebContextMenuData::MediaControls; 262 } else if (isHTMLObjectElement(*r.innerNonSharedNode()) || isHTMLEmbedElement(*r.innerNonSharedNode())) { 263 RenderObject* object = r.innerNonSharedNode()->renderer(); 264 if (object && object->isWidget()) { 265 Widget* widget = toRenderWidget(object)->widget(); 266 if (widget && widget->isPluginContainer()) { 267 data.mediaType = WebContextMenuData::MediaTypePlugin; 268 WebPluginContainerImpl* plugin = toWebPluginContainerImpl(widget); 269 WebString text = plugin->plugin()->selectionAsText(); 270 if (!text.isEmpty()) { 271 data.selectedText = text; 272 data.editFlags |= WebContextMenuData::CanCopy; 273 } 274 data.editFlags &= ~WebContextMenuData::CanTranslate; 275 data.linkURL = plugin->plugin()->linkAtPosition(data.mousePosition); 276 if (plugin->plugin()->supportsPaginatedPrint()) 277 data.mediaFlags |= WebContextMenuData::MediaCanPrint; 278 279 HTMLPlugInElement* pluginElement = toHTMLPlugInElement(r.innerNonSharedNode()); 280 data.srcURL = pluginElement->document().completeURL(pluginElement->url()); 281 data.mediaFlags |= WebContextMenuData::MediaCanSave; 282 283 // Add context menu commands that are supported by the plugin. 284 if (plugin->plugin()->canRotateView()) 285 data.mediaFlags |= WebContextMenuData::MediaCanRotate; 286 } 287 } 288 } 289 290 // An image can to be null for many reasons, like being blocked, no image 291 // data received from server yet. 292 data.hasImageContents = 293 (data.mediaType == WebContextMenuData::MediaTypeImage) 294 && r.image() && !(r.image()->isNull()); 295 296 // If it's not a link, an image, a media element, or an image/media link, 297 // show a selection menu or a more generic page menu. 298 if (selectedFrame->document()->loader()) 299 data.frameEncoding = selectedFrame->document()->encodingName(); 300 301 // Send the frame and page URLs in any case. 302 data.pageURL = urlFromFrame(m_webView->mainFrameImpl()->frame()); 303 if (selectedFrame != m_webView->mainFrameImpl()->frame()) { 304 data.frameURL = urlFromFrame(selectedFrame); 305 RefPtr<HistoryItem> historyItem = selectedFrame->loader().currentItem(); 306 if (historyItem) 307 data.frameHistoryItem = WebHistoryItem(historyItem); 308 } 309 310 if (r.isSelected()) { 311 if (!isHTMLInputElement(*r.innerNonSharedNode()) || !toHTMLInputElement(r.innerNonSharedNode())->isPasswordField()) 312 data.selectedText = selectedFrame->selectedText().stripWhiteSpace(); 313 } 314 315 if (r.isContentEditable()) { 316 data.isEditable = true; 317 318 // When Chrome enables asynchronous spellchecking, its spellchecker adds spelling markers to misspelled 319 // words and attaches suggestions to these markers in the background. Therefore, when a user right-clicks 320 // a mouse on a word, Chrome just needs to find a spelling marker on the word instead of spellchecking it. 321 if (selectedFrame->settings() && selectedFrame->settings()->asynchronousSpellCheckingEnabled()) { 322 DocumentMarker marker; 323 data.misspelledWord = selectMisspellingAsync(selectedFrame, marker); 324 data.misspellingHash = marker.hash(); 325 if (marker.description().length()) { 326 Vector<String> suggestions; 327 marker.description().split('\n', suggestions); 328 data.dictionarySuggestions = suggestions; 329 } else if (m_webView->spellCheckClient()) { 330 int misspelledOffset, misspelledLength; 331 m_webView->spellCheckClient()->spellCheck(data.misspelledWord, misspelledOffset, misspelledLength, &data.dictionarySuggestions); 332 } 333 } else { 334 data.isSpellCheckingEnabled = 335 toLocalFrame(m_webView->focusedWebCoreFrame())->spellChecker().isContinuousSpellCheckingEnabled(); 336 // Spellchecking might be enabled for the field, but could be disabled on the node. 337 if (toLocalFrame(m_webView->focusedWebCoreFrame())->spellChecker().isSpellCheckingEnabledInFocusedNode()) { 338 data.misspelledWord = selectMisspelledWord(selectedFrame); 339 if (m_webView->spellCheckClient()) { 340 int misspelledOffset, misspelledLength; 341 m_webView->spellCheckClient()->spellCheck( 342 data.misspelledWord, misspelledOffset, misspelledLength, 343 &data.dictionarySuggestions); 344 if (!misspelledLength) 345 data.misspelledWord.reset(); 346 } 347 } 348 } 349 HTMLFormElement* form = selectedFrame->selection().currentForm(); 350 if (form && isHTMLInputElement(*r.innerNonSharedNode())) { 351 HTMLInputElement& selectedElement = toHTMLInputElement(*r.innerNonSharedNode()); 352 WebSearchableFormData ws = WebSearchableFormData(WebFormElement(form), WebInputElement(&selectedElement)); 353 if (ws.url().isValid()) 354 data.keywordURL = ws.url(); 355 } 356 } 357 358 if (selectedFrame->editor().selectionHasStyle(CSSPropertyDirection, "ltr") != FalseTriState) 359 data.writingDirectionLeftToRight |= WebContextMenuData::CheckableMenuItemChecked; 360 if (selectedFrame->editor().selectionHasStyle(CSSPropertyDirection, "rtl") != FalseTriState) 361 data.writingDirectionRightToLeft |= WebContextMenuData::CheckableMenuItemChecked; 362 363 // Now retrieve the security info. 364 DocumentLoader* dl = selectedFrame->loader().documentLoader(); 365 WebDataSource* ds = WebDataSourceImpl::fromDocumentLoader(dl); 366 if (ds) 367 data.securityInfo = ds->response().securityInfo(); 368 369 data.referrerPolicy = static_cast<WebReferrerPolicy>(selectedFrame->document()->referrerPolicy()); 370 371 // Filter out custom menu elements and add them into the data. 372 populateCustomMenuItems(defaultMenu, &data); 373 374 data.node = r.innerNonSharedNode(); 375 376 WebLocalFrameImpl* selectedWebFrame = WebLocalFrameImpl::fromFrame(selectedFrame); 377 if (selectedWebFrame->client()) 378 selectedWebFrame->client()->showContextMenu(data); 379} 380 381void ContextMenuClientImpl::clearContextMenu() 382{ 383 HitTestResult r = m_webView->page()->contextMenuController().hitTestResult(); 384 LocalFrame* selectedFrame = r.innerNodeFrame(); 385 if (!selectedFrame) 386 return; 387 388 WebLocalFrameImpl* selectedWebFrame = WebLocalFrameImpl::fromFrame(selectedFrame); 389 if (selectedWebFrame->client()) 390 selectedWebFrame->client()->clearContextMenu(); 391} 392 393static void populateSubMenuItems(const Vector<ContextMenuItem>& inputMenu, WebVector<WebMenuItemInfo>& subMenuItems) 394{ 395 Vector<WebMenuItemInfo> subItems; 396 for (size_t i = 0; i < inputMenu.size(); ++i) { 397 const ContextMenuItem* inputItem = &inputMenu.at(i); 398 if (inputItem->action() < ContextMenuItemBaseCustomTag || inputItem->action() > ContextMenuItemLastCustomTag) 399 continue; 400 401 WebMenuItemInfo outputItem; 402 outputItem.label = inputItem->title(); 403 outputItem.enabled = inputItem->enabled(); 404 outputItem.checked = inputItem->checked(); 405 outputItem.action = static_cast<unsigned>(inputItem->action() - ContextMenuItemBaseCustomTag); 406 switch (inputItem->type()) { 407 case ActionType: 408 outputItem.type = WebMenuItemInfo::Option; 409 break; 410 case CheckableActionType: 411 outputItem.type = WebMenuItemInfo::CheckableOption; 412 break; 413 case SeparatorType: 414 outputItem.type = WebMenuItemInfo::Separator; 415 break; 416 case SubmenuType: 417 outputItem.type = WebMenuItemInfo::SubMenu; 418 populateSubMenuItems(inputItem->subMenuItems(), outputItem.subMenuItems); 419 break; 420 } 421 subItems.append(outputItem); 422 } 423 424 WebVector<WebMenuItemInfo> outputItems(subItems.size()); 425 for (size_t i = 0; i < subItems.size(); ++i) 426 outputItems[i] = subItems[i]; 427 subMenuItems.swap(outputItems); 428} 429 430void ContextMenuClientImpl::populateCustomMenuItems(const WebCore::ContextMenu* defaultMenu, WebContextMenuData* data) 431{ 432 populateSubMenuItems(defaultMenu->items(), data->customItems); 433} 434 435} // namespace blink 436