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 "ContextMenuClientImpl.h" 33 34#include "CSSPropertyNames.h" 35#include "HTMLNames.h" 36#include "WebContextMenuData.h" 37#include "WebDataSourceImpl.h" 38#include "WebFormElement.h" 39#include "WebFrameImpl.h" 40#include "WebMenuItemInfo.h" 41#include "WebPlugin.h" 42#include "WebPluginContainerImpl.h" 43#include "WebSearchableFormData.h" 44#include "WebSpellCheckClient.h" 45#include "WebViewClient.h" 46#include "WebViewImpl.h" 47#include "bindings/v8/ExceptionStatePlaceholder.h" 48#include "core/css/CSSStyleDeclaration.h" 49#include "core/dom/Document.h" 50#include "core/dom/DocumentMarkerController.h" 51#include "core/editing/Editor.h" 52#include "core/editing/SpellChecker.h" 53#include "core/history/HistoryItem.h" 54#include "core/html/HTMLFormElement.h" 55#include "core/html/HTMLInputElement.h" 56#include "core/html/HTMLMediaElement.h" 57#include "core/html/HTMLPlugInElement.h" 58#include "core/html/HTMLVideoElement.h" 59#include "core/html/MediaError.h" 60#include "core/loader/DocumentLoader.h" 61#include "core/loader/FrameLoader.h" 62#include "core/page/ContextMenuController.h" 63#include "core/page/EventHandler.h" 64#include "core/frame/FrameView.h" 65#include "core/page/Page.h" 66#include "core/frame/Settings.h" 67#include "core/rendering/HitTestResult.h" 68#include "core/rendering/RenderWidget.h" 69#include "platform/ContextMenu.h" 70#include "platform/Widget.h" 71#include "platform/text/TextBreakIterator.h" 72#include "platform/weborigin/KURL.h" 73#include "public/platform/WebPoint.h" 74#include "public/platform/WebString.h" 75#include "public/platform/WebURL.h" 76#include "public/platform/WebURLResponse.h" 77#include "public/platform/WebVector.h" 78#include "wtf/text/WTFString.h" 79 80using namespace WebCore; 81 82namespace blink { 83 84// Figure out the URL of a page or subframe. Returns |page_type| as the type, 85// which indicates page or subframe, or ContextNodeType::NONE if the URL could not 86// be determined for some reason. 87static WebURL urlFromFrame(Frame* frame) 88{ 89 if (frame) { 90 DocumentLoader* dl = frame->loader().documentLoader(); 91 if (dl) { 92 WebDataSource* ds = WebDataSourceImpl::fromDocumentLoader(dl); 93 if (ds) 94 return ds->hasUnreachableURL() ? ds->unreachableURL() : ds->request().url(); 95 } 96 } 97 return WebURL(); 98} 99 100// Helper function to determine whether text is a single word. 101static bool isASingleWord(const String& text) 102{ 103 TextBreakIterator* it = wordBreakIterator(text, 0, text.length()); 104 return it && it->next() == static_cast<int>(text.length()); 105} 106 107// Helper function to get misspelled word on which context menu 108// is to be invoked. This function also sets the word on which context menu 109// has been invoked to be the selected word, as required. This function changes 110// the selection only when there were no selected characters on OS X. 111static String selectMisspelledWord(Frame* selectedFrame) 112{ 113 // First select from selectedText to check for multiple word selection. 114 String misspelledWord = selectedFrame->selectedText().stripWhiteSpace(); 115 116 // If some texts were already selected, we don't change the selection. 117 if (!misspelledWord.isEmpty()) { 118 // Don't provide suggestions for multiple words. 119 if (!isASingleWord(misspelledWord)) 120 return String(); 121 return misspelledWord; 122 } 123 124 // Selection is empty, so change the selection to the word under the cursor. 125 HitTestResult hitTestResult = selectedFrame->eventHandler(). 126 hitTestResultAtPoint(selectedFrame->page()->contextMenuController().hitTestResult().pointInInnerNodeFrame()); 127 Node* innerNode = hitTestResult.innerNode(); 128 VisiblePosition pos(innerNode->renderer()->positionForPoint( 129 hitTestResult.localPoint())); 130 131 if (pos.isNull()) 132 return misspelledWord; // It is empty. 133 134 WebFrameImpl::selectWordAroundPosition(selectedFrame, pos); 135 misspelledWord = selectedFrame->selectedText().stripWhiteSpace(); 136 137#if OS(MACOSX) 138 // If misspelled word is still empty, then that portion should not be 139 // selected. Set the selection to that position only, and do not expand. 140 if (misspelledWord.isEmpty()) 141 selectedFrame->selection().setSelection(VisibleSelection(pos)); 142#else 143 // On non-Mac, right-click should not make a range selection in any case. 144 selectedFrame->selection().setSelection(VisibleSelection(pos)); 145#endif 146 return misspelledWord; 147} 148 149static bool IsWhiteSpaceOrPunctuation(UChar c) 150{ 151 return isSpaceOrNewline(c) || WTF::Unicode::isPunct(c); 152} 153 154static String selectMisspellingAsync(Frame* selectedFrame, DocumentMarker& marker) 155{ 156 VisibleSelection selection = selectedFrame->selection().selection(); 157 if (!selection.isCaretOrRange()) 158 return String(); 159 160 // Caret and range selections always return valid normalized ranges. 161 RefPtr<Range> selectionRange = selection.toNormalizedRange(); 162 Vector<DocumentMarker*> markers = selectedFrame->document()->markers()->markersInRange(selectionRange.get(), DocumentMarker::MisspellingMarkers()); 163 if (markers.size() != 1) 164 return String(); 165 marker = *markers[0]; 166 167 // Cloning a range fails only for invalid ranges. 168 RefPtr<Range> markerRange = selectionRange->cloneRange(ASSERT_NO_EXCEPTION); 169 markerRange->setStart(markerRange->startContainer(), marker.startOffset()); 170 markerRange->setEnd(markerRange->endContainer(), marker.endOffset()); 171 172 if (markerRange->text().stripWhiteSpace(&IsWhiteSpaceOrPunctuation) != selectionRange->text().stripWhiteSpace(&IsWhiteSpaceOrPunctuation)) 173 return String(); 174 175 return markerRange->text(); 176} 177 178void ContextMenuClientImpl::showContextMenu(const WebCore::ContextMenu* defaultMenu) 179{ 180 // Displaying the context menu in this function is a big hack as we don't 181 // have context, i.e. whether this is being invoked via a script or in 182 // response to user input (Mouse event WM_RBUTTONDOWN, 183 // Keyboard events KeyVK_APPS, Shift+F10). Check if this is being invoked 184 // in response to the above input events before popping up the context menu. 185 if (!m_webView->contextMenuAllowed()) 186 return; 187 188 HitTestResult r = m_webView->page()->contextMenuController().hitTestResult(); 189 Frame* selectedFrame = r.innerNodeFrame(); 190 191 WebContextMenuData data; 192 IntPoint mousePoint = selectedFrame->view()->contentsToWindow(r.roundedPointInInnerNodeFrame()); 193 mousePoint.scale(m_webView->pageScaleFactor(), m_webView->pageScaleFactor()); 194 data.mousePosition = mousePoint; 195 196 // Compute edit flags. 197 data.editFlags = WebContextMenuData::CanDoNone; 198 if (m_webView->focusedWebCoreFrame()->editor().canUndo()) 199 data.editFlags |= WebContextMenuData::CanUndo; 200 if (m_webView->focusedWebCoreFrame()->editor().canRedo()) 201 data.editFlags |= WebContextMenuData::CanRedo; 202 if (m_webView->focusedWebCoreFrame()->editor().canCut()) 203 data.editFlags |= WebContextMenuData::CanCut; 204 if (m_webView->focusedWebCoreFrame()->editor().canCopy()) 205 data.editFlags |= WebContextMenuData::CanCopy; 206 if (m_webView->focusedWebCoreFrame()->editor().canPaste()) 207 data.editFlags |= WebContextMenuData::CanPaste; 208 if (m_webView->focusedWebCoreFrame()->editor().canDelete()) 209 data.editFlags |= WebContextMenuData::CanDelete; 210 // We can always select all... 211 data.editFlags |= WebContextMenuData::CanSelectAll; 212 data.editFlags |= WebContextMenuData::CanTranslate; 213 214 // Links, Images, Media tags, and Image/Media-Links take preference over 215 // all else. 216 data.linkURL = r.absoluteLinkURL(); 217 218 if (!r.absoluteImageURL().isEmpty()) { 219 data.srcURL = r.absoluteImageURL(); 220 data.mediaType = WebContextMenuData::MediaTypeImage; 221 data.mediaFlags |= WebContextMenuData::MediaCanPrint; 222 } else if (!r.absoluteMediaURL().isEmpty()) { 223 data.srcURL = r.absoluteMediaURL(); 224 225 // We know that if absoluteMediaURL() is not empty, then this 226 // is a media element. 227 HTMLMediaElement* mediaElement = toHTMLMediaElement(r.innerNonSharedNode()); 228 if (isHTMLVideoElement(mediaElement)) 229 data.mediaType = WebContextMenuData::MediaTypeVideo; 230 else if (mediaElement->hasTagName(HTMLNames::audioTag)) 231 data.mediaType = WebContextMenuData::MediaTypeAudio; 232 233 if (mediaElement->error()) 234 data.mediaFlags |= WebContextMenuData::MediaInError; 235 if (mediaElement->paused()) 236 data.mediaFlags |= WebContextMenuData::MediaPaused; 237 if (mediaElement->muted()) 238 data.mediaFlags |= WebContextMenuData::MediaMuted; 239 if (mediaElement->loop()) 240 data.mediaFlags |= WebContextMenuData::MediaLoop; 241 if (mediaElement->supportsSave()) 242 data.mediaFlags |= WebContextMenuData::MediaCanSave; 243 if (mediaElement->hasAudio()) 244 data.mediaFlags |= WebContextMenuData::MediaHasAudio; 245 if (mediaElement->hasVideo()) 246 data.mediaFlags |= WebContextMenuData::MediaHasVideo; 247 if (mediaElement->controls()) 248 data.mediaFlags |= WebContextMenuData::MediaControls; 249 } else if (r.innerNonSharedNode()->hasTagName(HTMLNames::objectTag) 250 || r.innerNonSharedNode()->hasTagName(HTMLNames::embedTag)) { 251 RenderObject* object = r.innerNonSharedNode()->renderer(); 252 if (object && object->isWidget()) { 253 Widget* widget = toRenderWidget(object)->widget(); 254 if (widget && widget->isPluginContainer()) { 255 data.mediaType = WebContextMenuData::MediaTypePlugin; 256 WebPluginContainerImpl* plugin = toPluginContainerImpl(widget); 257 WebString text = plugin->plugin()->selectionAsText(); 258 if (!text.isEmpty()) { 259 data.selectedText = text; 260 data.editFlags |= WebContextMenuData::CanCopy; 261 } 262 data.editFlags &= ~WebContextMenuData::CanTranslate; 263 data.linkURL = plugin->plugin()->linkAtPosition(data.mousePosition); 264 if (plugin->plugin()->supportsPaginatedPrint()) 265 data.mediaFlags |= WebContextMenuData::MediaCanPrint; 266 267 HTMLPlugInElement* pluginElement = toHTMLPlugInElement(r.innerNonSharedNode()); 268 data.srcURL = pluginElement->document().completeURL(pluginElement->url()); 269 data.mediaFlags |= WebContextMenuData::MediaCanSave; 270 271 // Add context menu commands that are supported by the plugin. 272 if (plugin->plugin()->canRotateView()) 273 data.mediaFlags |= WebContextMenuData::MediaCanRotate; 274 } 275 } 276 } 277 278 // An image can to be null for many reasons, like being blocked, no image 279 // data received from server yet. 280 data.hasImageContents = 281 (data.mediaType == WebContextMenuData::MediaTypeImage) 282 && r.image() && !(r.image()->isNull()); 283 284 // If it's not a link, an image, a media element, or an image/media link, 285 // show a selection menu or a more generic page menu. 286 if (selectedFrame->document()->loader()) 287 data.frameEncoding = selectedFrame->document()->encodingName(); 288 289 // Send the frame and page URLs in any case. 290 data.pageURL = urlFromFrame(m_webView->mainFrameImpl()->frame()); 291 if (selectedFrame != m_webView->mainFrameImpl()->frame()) { 292 data.frameURL = urlFromFrame(selectedFrame); 293 RefPtr<HistoryItem> historyItem = selectedFrame->loader().currentItem(); 294 if (historyItem) 295 data.frameHistoryItem = WebHistoryItem(historyItem); 296 } 297 298 if (r.isSelected()) { 299 if (!r.innerNonSharedNode()->hasTagName(HTMLNames::inputTag) || !toHTMLInputElement(r.innerNonSharedNode())->isPasswordField()) 300 data.selectedText = selectedFrame->selectedText().stripWhiteSpace(); 301 } 302 303 if (r.isContentEditable()) { 304 data.isEditable = true; 305#if ENABLE(INPUT_SPEECH) 306 if (r.innerNonSharedNode()->hasTagName(HTMLNames::inputTag)) 307 data.isSpeechInputEnabled = toHTMLInputElement(r.innerNonSharedNode())->isSpeechEnabled(); 308#endif 309 // When Chrome enables asynchronous spellchecking, its spellchecker adds spelling markers to misspelled 310 // words and attaches suggestions to these markers in the background. Therefore, when a user right-clicks 311 // a mouse on a word, Chrome just needs to find a spelling marker on the word instead of spellchecking it. 312 if (selectedFrame->settings() && selectedFrame->settings()->asynchronousSpellCheckingEnabled()) { 313 DocumentMarker marker; 314 data.misspelledWord = selectMisspellingAsync(selectedFrame, marker); 315 data.misspellingHash = marker.hash(); 316 if (marker.description().length()) { 317 Vector<String> suggestions; 318 marker.description().split('\n', suggestions); 319 data.dictionarySuggestions = suggestions; 320 } else if (m_webView->spellCheckClient()) { 321 int misspelledOffset, misspelledLength; 322 m_webView->spellCheckClient()->spellCheck(data.misspelledWord, misspelledOffset, misspelledLength, &data.dictionarySuggestions); 323 } 324 } else { 325 data.isSpellCheckingEnabled = 326 m_webView->focusedWebCoreFrame()->spellChecker().isContinuousSpellCheckingEnabled(); 327 // Spellchecking might be enabled for the field, but could be disabled on the node. 328 if (m_webView->focusedWebCoreFrame()->spellChecker().isSpellCheckingEnabledInFocusedNode()) { 329 data.misspelledWord = selectMisspelledWord(selectedFrame); 330 if (m_webView->spellCheckClient()) { 331 int misspelledOffset, misspelledLength; 332 m_webView->spellCheckClient()->spellCheck( 333 data.misspelledWord, misspelledOffset, misspelledLength, 334 &data.dictionarySuggestions); 335 if (!misspelledLength) 336 data.misspelledWord.reset(); 337 } 338 } 339 } 340 HTMLFormElement* form = selectedFrame->selection().currentForm(); 341 if (form && r.innerNonSharedNode()->hasTagName(HTMLNames::inputTag)) { 342 HTMLInputElement* selectedElement = toHTMLInputElement(r.innerNonSharedNode()); 343 if (selectedElement) { 344 WebSearchableFormData ws = WebSearchableFormData(WebFormElement(form), WebInputElement(selectedElement)); 345 if (ws.url().isValid()) 346 data.keywordURL = ws.url(); 347 } 348 } 349 } 350 351 if (selectedFrame->editor().selectionHasStyle(CSSPropertyDirection, "ltr") != FalseTriState) 352 data.writingDirectionLeftToRight |= WebContextMenuData::CheckableMenuItemChecked; 353 if (selectedFrame->editor().selectionHasStyle(CSSPropertyDirection, "rtl") != FalseTriState) 354 data.writingDirectionRightToLeft |= WebContextMenuData::CheckableMenuItemChecked; 355 356 // Now retrieve the security info. 357 DocumentLoader* dl = selectedFrame->loader().documentLoader(); 358 WebDataSource* ds = WebDataSourceImpl::fromDocumentLoader(dl); 359 if (ds) 360 data.securityInfo = ds->response().securityInfo(); 361 362 data.referrerPolicy = static_cast<WebReferrerPolicy>(selectedFrame->document()->referrerPolicy()); 363 364 // Filter out custom menu elements and add them into the data. 365 populateCustomMenuItems(defaultMenu, &data); 366 367 data.node = r.innerNonSharedNode(); 368 369 WebFrame* selected_web_frame = WebFrameImpl::fromFrame(selectedFrame); 370 if (m_webView->client()) 371 m_webView->client()->showContextMenu(selected_web_frame, data); 372} 373 374void ContextMenuClientImpl::clearContextMenu() 375{ 376 if (m_webView->client()) 377 m_webView->client()->clearContextMenu(); 378} 379 380static void populateSubMenuItems(const Vector<ContextMenuItem>& inputMenu, WebVector<WebMenuItemInfo>& subMenuItems) 381{ 382 Vector<WebMenuItemInfo> subItems; 383 for (size_t i = 0; i < inputMenu.size(); ++i) { 384 const ContextMenuItem* inputItem = &inputMenu.at(i); 385 if (inputItem->action() < ContextMenuItemBaseCustomTag || inputItem->action() > ContextMenuItemLastCustomTag) 386 continue; 387 388 WebMenuItemInfo outputItem; 389 outputItem.label = inputItem->title(); 390 outputItem.enabled = inputItem->enabled(); 391 outputItem.checked = inputItem->checked(); 392 outputItem.action = static_cast<unsigned>(inputItem->action() - ContextMenuItemBaseCustomTag); 393 switch (inputItem->type()) { 394 case ActionType: 395 outputItem.type = WebMenuItemInfo::Option; 396 break; 397 case CheckableActionType: 398 outputItem.type = WebMenuItemInfo::CheckableOption; 399 break; 400 case SeparatorType: 401 outputItem.type = WebMenuItemInfo::Separator; 402 break; 403 case SubmenuType: 404 outputItem.type = WebMenuItemInfo::SubMenu; 405 populateSubMenuItems(inputItem->subMenuItems(), outputItem.subMenuItems); 406 break; 407 } 408 subItems.append(outputItem); 409 } 410 411 WebVector<WebMenuItemInfo> outputItems(subItems.size()); 412 for (size_t i = 0; i < subItems.size(); ++i) 413 outputItems[i] = subItems[i]; 414 subMenuItems.swap(outputItems); 415} 416 417void ContextMenuClientImpl::populateCustomMenuItems(const WebCore::ContextMenu* defaultMenu, WebContextMenuData* data) 418{ 419 populateSubMenuItems(defaultMenu->items(), data->customItems); 420} 421 422} // namespace blink 423