// Copyright (c) 2012 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 "chrome/renderer/chrome_render_view_observer.h" #include "base/bind.h" #include "base/bind_helpers.h" #include "base/command_line.h" #include "base/debug/trace_event.h" #include "base/message_loop/message_loop.h" #include "base/metrics/histogram.h" #include "base/strings/string_split.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "chrome/common/chrome_constants.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/prerender_messages.h" #include "chrome/common/render_messages.h" #include "chrome/common/url_constants.h" #include "chrome/renderer/chrome_render_process_observer.h" #include "chrome/renderer/external_host_bindings.h" #include "chrome/renderer/prerender/prerender_helper.h" #include "chrome/renderer/safe_browsing/phishing_classifier_delegate.h" #include "chrome/renderer/translate/translate_helper.h" #include "chrome/renderer/webview_color_overlay.h" #include "content/public/common/bindings_policy.h" #include "content/public/renderer/content_renderer_client.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_view.h" #include "extensions/common/constants.h" #include "extensions/common/stack_frame.h" #include "net/base/data_url.h" #include "skia/ext/image_operations.h" #include "skia/ext/platform_canvas.h" #include "third_party/WebKit/public/platform/WebCString.h" #include "third_party/WebKit/public/platform/WebRect.h" #include "third_party/WebKit/public/platform/WebSize.h" #include "third_party/WebKit/public/platform/WebString.h" #include "third_party/WebKit/public/platform/WebURLRequest.h" #include "third_party/WebKit/public/platform/WebVector.h" #include "third_party/WebKit/public/web/WebAXObject.h" #include "third_party/WebKit/public/web/WebDataSource.h" #include "third_party/WebKit/public/web/WebDocument.h" #include "third_party/WebKit/public/web/WebElement.h" #include "third_party/WebKit/public/web/WebFrame.h" #include "third_party/WebKit/public/web/WebInputEvent.h" #include "third_party/WebKit/public/web/WebNode.h" #include "third_party/WebKit/public/web/WebNodeList.h" #include "third_party/WebKit/public/web/WebView.h" #include "ui/base/ui_base_switches_util.h" #include "ui/gfx/favicon_size.h" #include "ui/gfx/size.h" #include "ui/gfx/size_f.h" #include "ui/gfx/skbitmap_operations.h" #include "v8/include/v8-testing.h" using blink::WebAXObject; using blink::WebCString; using blink::WebDataSource; using blink::WebDocument; using blink::WebElement; using blink::WebFrame; using blink::WebGestureEvent; using blink::WebIconURL; using blink::WebNode; using blink::WebNodeList; using blink::WebRect; using blink::WebSecurityOrigin; using blink::WebSize; using blink::WebString; using blink::WebTouchEvent; using blink::WebURL; using blink::WebURLRequest; using blink::WebView; using blink::WebVector; using blink::WebWindowFeatures; // Delay in milliseconds that we'll wait before capturing the page contents // and thumbnail. static const int kDelayForCaptureMs = 500; // Typically, we capture the page data once the page is loaded. // Sometimes, the page never finishes to load, preventing the page capture // To workaround this problem, we always perform a capture after the following // delay. static const int kDelayForForcedCaptureMs = 6000; // define to write the time necessary for thumbnail/DOM text retrieval, // respectively, into the system debug log // #define TIME_TEXT_RETRIEVAL // maximum number of characters in the document to index, any text beyond this // point will be clipped static const size_t kMaxIndexChars = 65535; // Constants for UMA statistic collection. static const char kTranslateCaptureText[] = "Translate.CaptureText"; namespace { GURL StripRef(const GURL& url) { GURL::Replacements replacements; replacements.ClearRef(); return url.ReplaceComponents(replacements); } // If the source image is null or occupies less area than // |thumbnail_min_area_pixels|, we return the image unmodified. Otherwise, we // scale down the image so that the width and height do not exceed // |thumbnail_max_size_pixels|, preserving the original aspect ratio. SkBitmap Downscale(blink::WebImage image, int thumbnail_min_area_pixels, gfx::Size thumbnail_max_size_pixels) { if (image.isNull()) return SkBitmap(); gfx::Size image_size = image.size(); if (image_size.GetArea() < thumbnail_min_area_pixels) return image.getSkBitmap(); if (image_size.width() <= thumbnail_max_size_pixels.width() && image_size.height() <= thumbnail_max_size_pixels.height()) return image.getSkBitmap(); gfx::SizeF scaled_size = image_size; if (scaled_size.width() > thumbnail_max_size_pixels.width()) { scaled_size.Scale(thumbnail_max_size_pixels.width() / scaled_size.width()); } if (scaled_size.height() > thumbnail_max_size_pixels.height()) { scaled_size.Scale( thumbnail_max_size_pixels.height() / scaled_size.height()); } return skia::ImageOperations::Resize(image.getSkBitmap(), skia::ImageOperations::RESIZE_GOOD, static_cast<int>(scaled_size.width()), static_cast<int>(scaled_size.height())); } // The delimiter for a stack trace provided by WebKit. const char kStackFrameDelimiter[] = "\n at "; // Get a stack trace from a WebKit console message. // There are three possible scenarios: // 1. WebKit gives us a stack trace in |stack_trace|. // 2. The stack trace is embedded in the error |message| by an internal // script. This will be more useful than |stack_trace|, since |stack_trace| // will include the internal bindings trace, instead of a developer's code. // 3. No stack trace is included. In this case, we should mock one up from // the given line number and source. // |message| will be populated with the error message only (i.e., will not // include any stack trace). extensions::StackTrace GetStackTraceFromMessage( base::string16* message, const base::string16& source, const base::string16& stack_trace, int32 line_number) { extensions::StackTrace result; std::vector<base::string16> pieces; size_t index = 0; if (message->find(base::UTF8ToUTF16(kStackFrameDelimiter)) != base::string16::npos) { base::SplitStringUsingSubstr(*message, base::UTF8ToUTF16(kStackFrameDelimiter), &pieces); *message = pieces[0]; index = 1; } else if (!stack_trace.empty()) { base::SplitStringUsingSubstr(stack_trace, base::UTF8ToUTF16(kStackFrameDelimiter), &pieces); } // If we got a stack trace, parse each frame from the text. if (index < pieces.size()) { for (; index < pieces.size(); ++index) { scoped_ptr<extensions::StackFrame> frame = extensions::StackFrame::CreateFromText(pieces[index]); if (frame.get()) result.push_back(*frame); } } if (result.empty()) { // If we don't have a stack trace, mock one up. result.push_back( extensions::StackFrame(line_number, 1u, // column number source, base::string16() /* no function name */ )); } return result; } } // namespace ChromeRenderViewObserver::ChromeRenderViewObserver( content::RenderView* render_view, ChromeRenderProcessObserver* chrome_render_process_observer) : content::RenderViewObserver(render_view), chrome_render_process_observer_(chrome_render_process_observer), translate_helper_(new TranslateHelper(render_view)), phishing_classifier_(NULL), last_indexed_page_id_(-1), capture_timer_(false, false) { const CommandLine& command_line = *CommandLine::ForCurrentProcess(); if (!command_line.HasSwitch(switches::kDisableClientSidePhishingDetection)) OnSetClientSidePhishingDetection(true); } ChromeRenderViewObserver::~ChromeRenderViewObserver() { } bool ChromeRenderViewObserver::OnMessageReceived(const IPC::Message& message) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(ChromeRenderViewObserver, message) IPC_MESSAGE_HANDLER(ChromeViewMsg_WebUIJavaScript, OnWebUIJavaScript) IPC_MESSAGE_HANDLER(ChromeViewMsg_HandleMessageFromExternalHost, OnHandleMessageFromExternalHost) IPC_MESSAGE_HANDLER(ChromeViewMsg_JavaScriptStressTestControl, OnJavaScriptStressTestControl) IPC_MESSAGE_HANDLER(ChromeViewMsg_SetClientSidePhishingDetection, OnSetClientSidePhishingDetection) IPC_MESSAGE_HANDLER(ChromeViewMsg_SetVisuallyDeemphasized, OnSetVisuallyDeemphasized) IPC_MESSAGE_HANDLER(ChromeViewMsg_RequestThumbnailForContextNode, OnRequestThumbnailForContextNode) IPC_MESSAGE_HANDLER(ChromeViewMsg_GetFPS, OnGetFPS) #if defined(OS_ANDROID) IPC_MESSAGE_HANDLER(ChromeViewMsg_UpdateTopControlsState, OnUpdateTopControlsState) IPC_MESSAGE_HANDLER(ChromeViewMsg_RetrieveWebappInformation, OnRetrieveWebappInformation) #endif IPC_MESSAGE_HANDLER(ChromeViewMsg_SetWindowFeatures, OnSetWindowFeatures) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; } void ChromeRenderViewObserver::OnWebUIJavaScript( const base::string16& frame_xpath, const base::string16& jscript, int id, bool notify_result) { webui_javascript_.reset(new WebUIJavaScript()); webui_javascript_->frame_xpath = frame_xpath; webui_javascript_->jscript = jscript; webui_javascript_->id = id; webui_javascript_->notify_result = notify_result; } void ChromeRenderViewObserver::OnHandleMessageFromExternalHost( const std::string& message, const std::string& origin, const std::string& target) { if (message.empty()) return; GetExternalHostBindings()->ForwardMessageFromExternalHost(message, origin, target); } void ChromeRenderViewObserver::OnJavaScriptStressTestControl(int cmd, int param) { if (cmd == kJavaScriptStressTestSetStressRunType) { v8::Testing::SetStressRunType(static_cast<v8::Testing::StressType>(param)); } else if (cmd == kJavaScriptStressTestPrepareStressRun) { v8::Testing::PrepareStressRun(param); } } #if defined(OS_ANDROID) void ChromeRenderViewObserver::OnUpdateTopControlsState( content::TopControlsState constraints, content::TopControlsState current, bool animate) { render_view()->UpdateTopControlsState(constraints, current, animate); } void ChromeRenderViewObserver::OnRetrieveWebappInformation( const GURL& expected_url) { WebFrame* main_frame = render_view()->GetWebView()->mainFrame(); WebDocument document = main_frame ? main_frame->document() : WebDocument(); WebElement head = document.isNull() ? WebElement() : document.head(); GURL document_url = document.isNull() ? GURL() : GURL(document.url()); // Make sure we're checking the right page. bool success = document_url == expected_url; bool is_mobile_webapp_capable = false; bool is_apple_mobile_webapp_capable = false; // Search the DOM for the webapp <meta> tags. if (!head.isNull()) { WebNodeList children = head.childNodes(); for (unsigned i = 0; i < children.length(); ++i) { WebNode child = children.item(i); if (!child.isElementNode()) continue; WebElement elem = child.to<WebElement>(); if (elem.hasTagName("meta") && elem.hasAttribute("name")) { std::string name = elem.getAttribute("name").utf8(); WebString content = elem.getAttribute("content"); if (LowerCaseEqualsASCII(content, "yes")) { if (name == "mobile-web-app-capable") { is_mobile_webapp_capable = true; } else if (name == "apple-mobile-web-app-capable") { is_apple_mobile_webapp_capable = true; } } } } } else { success = false; } bool is_only_apple_mobile_webapp_capable = is_apple_mobile_webapp_capable && !is_mobile_webapp_capable; if (main_frame && is_only_apple_mobile_webapp_capable) { blink::WebConsoleMessage message( blink::WebConsoleMessage::LevelWarning, "<meta name=\"apple-mobile-web-app-capable\" content=\"yes\"> is " "deprecated. Please include <meta name=\"mobile-web-app-capable\" " "content=\"yes\"> - " "http://developers.google.com/chrome/mobile/docs/installtohomescreen"); main_frame->addMessageToConsole(message); } Send(new ChromeViewHostMsg_DidRetrieveWebappInformation( routing_id(), success, is_mobile_webapp_capable, is_apple_mobile_webapp_capable, expected_url)); } #endif void ChromeRenderViewObserver::OnSetWindowFeatures( const WebWindowFeatures& window_features) { render_view()->GetWebView()->setWindowFeatures(window_features); } void ChromeRenderViewObserver::Navigate(const GURL& url) { // Execute cache clear operations that were postponed until a navigation // event (including tab reload). if (chrome_render_process_observer_) chrome_render_process_observer_->ExecutePendingClearCache(); } void ChromeRenderViewObserver::OnSetClientSidePhishingDetection( bool enable_phishing_detection) { #if defined(FULL_SAFE_BROWSING) && !defined(OS_CHROMEOS) phishing_classifier_ = enable_phishing_detection ? safe_browsing::PhishingClassifierDelegate::Create( render_view(), NULL) : NULL; #endif } void ChromeRenderViewObserver::OnSetVisuallyDeemphasized(bool deemphasized) { bool already_deemphasized = !!dimmed_color_overlay_.get(); if (already_deemphasized == deemphasized) return; if (deemphasized) { // 70% opaque grey. SkColor greyish = SkColorSetARGB(178, 0, 0, 0); dimmed_color_overlay_.reset( new WebViewColorOverlay(render_view(), greyish)); } else { dimmed_color_overlay_.reset(); } } void ChromeRenderViewObserver::OnRequestThumbnailForContextNode( int thumbnail_min_area_pixels, gfx::Size thumbnail_max_size_pixels) { WebNode context_node = render_view()->GetContextMenuNode(); SkBitmap thumbnail; gfx::Size original_size; if (!context_node.isNull() && context_node.isElementNode()) { blink::WebImage image = context_node.to<WebElement>().imageContents(); original_size = image.size(); thumbnail = Downscale(image, thumbnail_min_area_pixels, thumbnail_max_size_pixels); } Send(new ChromeViewHostMsg_RequestThumbnailForContextNode_ACK( routing_id(), thumbnail, original_size)); } void ChromeRenderViewObserver::OnGetFPS() { float fps = (render_view()->GetFilteredTimePerFrame() > 0.0f)? 1.0f / render_view()->GetFilteredTimePerFrame() : 0.0f; Send(new ChromeViewHostMsg_FPS(routing_id(), fps)); } void ChromeRenderViewObserver::DidStartLoading() { if ((render_view()->GetEnabledBindings() & content::BINDINGS_POLICY_WEB_UI) && webui_javascript_.get()) { render_view()->EvaluateScript(webui_javascript_->frame_xpath, webui_javascript_->jscript, webui_javascript_->id, webui_javascript_->notify_result); webui_javascript_.reset(); } } void ChromeRenderViewObserver::DidStopLoading() { WebFrame* main_frame = render_view()->GetWebView()->mainFrame(); GURL osd_url = main_frame->document().openSearchDescriptionURL(); if (!osd_url.is_empty()) { Send(new ChromeViewHostMsg_PageHasOSDD( routing_id(), render_view()->GetPageId(), osd_url, search_provider::AUTODETECTED_PROVIDER)); } // Don't capture pages including refresh meta tag. if (HasRefreshMetaTag(main_frame)) return; CapturePageInfoLater( render_view()->GetPageId(), false, // preliminary_capture base::TimeDelta::FromMilliseconds( render_view()->GetContentStateImmediately() ? 0 : kDelayForCaptureMs)); } void ChromeRenderViewObserver::DidCommitProvisionalLoad( WebFrame* frame, bool is_new_navigation) { // Don't capture pages being not new, or including refresh meta tag. if (!is_new_navigation || HasRefreshMetaTag(frame)) return; CapturePageInfoLater( render_view()->GetPageId(), true, // preliminary_capture base::TimeDelta::FromMilliseconds(kDelayForForcedCaptureMs)); } void ChromeRenderViewObserver::DidClearWindowObject(WebFrame* frame) { if (render_view()->GetEnabledBindings() & content::BINDINGS_POLICY_EXTERNAL_HOST) { GetExternalHostBindings()->BindToJavascript(frame, "externalHost"); } } void ChromeRenderViewObserver::DetailedConsoleMessageAdded( const base::string16& message, const base::string16& source, const base::string16& stack_trace_string, int32 line_number, int32 severity_level) { base::string16 trimmed_message = message; extensions::StackTrace stack_trace = GetStackTraceFromMessage( &trimmed_message, source, stack_trace_string, line_number); Send(new ChromeViewHostMsg_DetailedConsoleMessageAdded(routing_id(), trimmed_message, source, stack_trace, severity_level)); } void ChromeRenderViewObserver::CapturePageInfoLater(int page_id, bool preliminary_capture, base::TimeDelta delay) { capture_timer_.Start( FROM_HERE, delay, base::Bind(&ChromeRenderViewObserver::CapturePageInfo, base::Unretained(this), page_id, preliminary_capture)); } void ChromeRenderViewObserver::CapturePageInfo(int page_id, bool preliminary_capture) { // If |page_id| is obsolete, we should stop indexing and capturing a page. if (render_view()->GetPageId() != page_id) return; if (!render_view()->GetWebView()) return; WebFrame* main_frame = render_view()->GetWebView()->mainFrame(); if (!main_frame) return; // Don't index/capture pages that are in view source mode. if (main_frame->isViewSourceModeEnabled()) return; // Don't index/capture pages that failed to load. This only checks the top // level frame so the thumbnail may contain a frame that failed to load. WebDataSource* ds = main_frame->dataSource(); if (ds && ds->hasUnreachableURL()) return; // Don't index/capture pages that are being prerendered. if (prerender::PrerenderHelper::IsPrerendering( render_view()->GetMainRenderFrame())) { return; } // Retrieve the frame's full text (up to kMaxIndexChars), and pass it to the // translate helper for language detection and possible translation. base::string16 contents; base::TimeTicks capture_begin_time = base::TimeTicks::Now(); CaptureText(main_frame, &contents); UMA_HISTOGRAM_TIMES(kTranslateCaptureText, base::TimeTicks::Now() - capture_begin_time); if (translate_helper_) translate_helper_->PageCaptured(page_id, contents); // TODO(shess): Is indexing "Full text search" indexing? In that // case more of this can go. // Skip indexing if this is not a new load. Note that the case where // page_id == last_indexed_page_id_ is more complicated, since we need to // reindex if the toplevel URL has changed (such as from a redirect), even // though this may not cause the page id to be incremented. if (page_id < last_indexed_page_id_) return; bool same_page_id = last_indexed_page_id_ == page_id; if (!preliminary_capture) last_indexed_page_id_ = page_id; // Get the URL for this page. GURL url(main_frame->document().url()); if (url.is_empty()) { if (!preliminary_capture) last_indexed_url_ = GURL(); return; } // If the page id is unchanged, check whether the URL (ignoring fragments) // has changed. If so, we need to reindex. Otherwise, assume this is a // reload, in-page navigation, or some other load type where we don't want to // reindex. Note: subframe navigations after onload increment the page id, // so these will trigger a reindex. GURL stripped_url(StripRef(url)); if (same_page_id && stripped_url == last_indexed_url_) return; if (!preliminary_capture) last_indexed_url_ = stripped_url; TRACE_EVENT0("renderer", "ChromeRenderViewObserver::CapturePageInfo"); #if defined(FULL_SAFE_BROWSING) // Will swap out the string. if (phishing_classifier_) phishing_classifier_->PageCaptured(&contents, preliminary_capture); #endif } void ChromeRenderViewObserver::CaptureText(WebFrame* frame, base::string16* contents) { contents->clear(); if (!frame) return; #ifdef TIME_TEXT_RETRIEVAL double begin = time_util::GetHighResolutionTimeNow(); #endif // get the contents of the frame *contents = frame->contentAsText(kMaxIndexChars); #ifdef TIME_TEXT_RETRIEVAL double end = time_util::GetHighResolutionTimeNow(); char buf[128]; sprintf_s(buf, "%d chars retrieved for indexing in %gms\n", contents.size(), (end - begin)*1000); OutputDebugStringA(buf); #endif // When the contents are clipped to the maximum, we don't want to have a // partial word indexed at the end that might have been clipped. Therefore, // terminate the string at the last space to ensure no words are clipped. if (contents->size() == kMaxIndexChars) { size_t last_space_index = contents->find_last_of(base::kWhitespaceUTF16); if (last_space_index == base::string16::npos) return; // don't index if we got a huge block of text with no spaces contents->resize(last_space_index); } } ExternalHostBindings* ChromeRenderViewObserver::GetExternalHostBindings() { if (!external_host_bindings_.get()) { external_host_bindings_.reset(new ExternalHostBindings( render_view(), routing_id())); } return external_host_bindings_.get(); } bool ChromeRenderViewObserver::HasRefreshMetaTag(WebFrame* frame) { if (!frame) return false; WebElement head = frame->document().head(); if (head.isNull() || !head.hasChildNodes()) return false; const WebString tag_name(ASCIIToUTF16("meta")); const WebString attribute_name(ASCIIToUTF16("http-equiv")); WebNodeList children = head.childNodes(); for (size_t i = 0; i < children.length(); ++i) { WebNode node = children.item(i); if (!node.isElementNode()) continue; WebElement element = node.to<WebElement>(); if (!element.hasTagName(tag_name)) continue; WebString value = element.getAttribute(attribute_name); if (value.isNull() || !LowerCaseEqualsASCII(value, "refresh")) continue; return true; } return false; }