1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "components/dom_distiller/content/dom_distiller_viewer_source.h" 6 7#include <sstream> 8#include <string> 9#include <vector> 10 11#include "base/memory/ref_counted_memory.h" 12#include "base/memory/scoped_ptr.h" 13#include "base/message_loop/message_loop.h" 14#include "base/metrics/user_metrics.h" 15#include "base/strings/utf_string_conversions.h" 16#include "components/dom_distiller/core/distilled_page_prefs.h" 17#include "components/dom_distiller/core/dom_distiller_service.h" 18#include "components/dom_distiller/core/task_tracker.h" 19#include "components/dom_distiller/core/url_constants.h" 20#include "components/dom_distiller/core/viewer.h" 21#include "content/public/browser/navigation_details.h" 22#include "content/public/browser/navigation_entry.h" 23#include "content/public/browser/render_frame_host.h" 24#include "content/public/browser/render_view_host.h" 25#include "content/public/browser/user_metrics.h" 26#include "content/public/browser/web_contents.h" 27#include "content/public/browser/web_contents_observer.h" 28#include "net/base/url_util.h" 29#include "net/url_request/url_request.h" 30 31namespace dom_distiller { 32 33// Handles receiving data asynchronously for a specific entry, and passing 34// it along to the data callback for the data source. Lifetime matches that of 35// the current main frame's page in the Viewer instance. 36class DomDistillerViewerSource::RequestViewerHandle 37 : public ViewRequestDelegate, 38 public content::WebContentsObserver, 39 public DistilledPagePrefs::Observer { 40 public: 41 explicit RequestViewerHandle( 42 content::WebContents* web_contents, 43 const std::string& expected_scheme, 44 const std::string& expected_request_path, 45 const content::URLDataSource::GotDataCallback& callback, 46 DistilledPagePrefs* distilled_page_prefs); 47 virtual ~RequestViewerHandle(); 48 49 // ViewRequestDelegate implementation: 50 virtual void OnArticleReady( 51 const DistilledArticleProto* article_proto) OVERRIDE; 52 53 virtual void OnArticleUpdated( 54 ArticleDistillationUpdate article_update) OVERRIDE; 55 56 void TakeViewerHandle(scoped_ptr<ViewerHandle> viewer_handle); 57 58 // content::WebContentsObserver implementation: 59 virtual void DidNavigateMainFrame( 60 const content::LoadCommittedDetails& details, 61 const content::FrameNavigateParams& params) OVERRIDE; 62 virtual void RenderProcessGone(base::TerminationStatus status) OVERRIDE; 63 virtual void WebContentsDestroyed() OVERRIDE; 64 virtual void DidFinishLoad(content::RenderFrameHost* render_frame_host, 65 const GURL& validated_url) OVERRIDE; 66 67 private: 68 // Sends JavaScript to the attached Viewer, buffering data if the viewer isn't 69 // ready. 70 void SendJavaScript(const std::string& buffer); 71 72 // Cancels the current view request. Once called, no updates will be 73 // propagated to the view, and the request to DomDistillerService will be 74 // cancelled. 75 void Cancel(); 76 77 // DistilledPagePrefs::Observer implementation: 78 virtual void OnChangeFontFamily( 79 DistilledPagePrefs::FontFamily new_font_family) OVERRIDE; 80 virtual void OnChangeTheme(DistilledPagePrefs::Theme new_theme) OVERRIDE; 81 82 // The handle to the view request towards the DomDistillerService. It 83 // needs to be kept around to ensure the distillation request finishes. 84 scoped_ptr<ViewerHandle> viewer_handle_; 85 86 // The scheme hosting the current view request; 87 std::string expected_scheme_; 88 89 // The query path for the current view request. 90 std::string expected_request_path_; 91 92 // Holds the callback to where the data retrieved is sent back. 93 content::URLDataSource::GotDataCallback callback_; 94 95 // Number of pages of the distilled article content that have been rendered by 96 // the viewer. 97 int page_count_; 98 99 // Interface for accessing preferences for distilled pages. 100 DistilledPagePrefs* distilled_page_prefs_; 101 102 // Whether the page is sufficiently initialized to handle updates from the 103 // distiller. 104 bool waiting_for_page_ready_; 105 106 // Temporary store of pending JavaScript if the page isn't ready to receive 107 // data from distillation. 108 std::string buffer_; 109}; 110 111DomDistillerViewerSource::RequestViewerHandle::RequestViewerHandle( 112 content::WebContents* web_contents, 113 const std::string& expected_scheme, 114 const std::string& expected_request_path, 115 const content::URLDataSource::GotDataCallback& callback, 116 DistilledPagePrefs* distilled_page_prefs) 117 : expected_scheme_(expected_scheme), 118 expected_request_path_(expected_request_path), 119 callback_(callback), 120 page_count_(0), 121 distilled_page_prefs_(distilled_page_prefs), 122 waiting_for_page_ready_(true) { 123 content::WebContentsObserver::Observe(web_contents); 124 distilled_page_prefs_->AddObserver(this); 125} 126 127DomDistillerViewerSource::RequestViewerHandle::~RequestViewerHandle() { 128 distilled_page_prefs_->RemoveObserver(this); 129} 130 131void DomDistillerViewerSource::RequestViewerHandle::SendJavaScript( 132 const std::string& buffer) { 133 if (waiting_for_page_ready_) { 134 buffer_ += buffer; 135 } else { 136 if (web_contents()) { 137 web_contents()->GetMainFrame()->ExecuteJavaScript( 138 base::UTF8ToUTF16(buffer)); 139 } 140 } 141} 142 143void DomDistillerViewerSource::RequestViewerHandle::DidNavigateMainFrame( 144 const content::LoadCommittedDetails& details, 145 const content::FrameNavigateParams& params) { 146 const GURL& navigation = details.entry->GetURL(); 147 if (details.is_in_page || ( 148 navigation.SchemeIs(expected_scheme_.c_str()) && 149 expected_request_path_ == navigation.query())) { 150 // In-page navigations, as well as the main view request can be ignored. 151 return; 152 } 153 154 Cancel(); 155 156} 157 158void DomDistillerViewerSource::RequestViewerHandle::RenderProcessGone( 159 base::TerminationStatus status) { 160 Cancel(); 161} 162 163void DomDistillerViewerSource::RequestViewerHandle::WebContentsDestroyed() { 164 Cancel(); 165} 166 167void DomDistillerViewerSource::RequestViewerHandle::Cancel() { 168 // No need to listen for notifications. 169 content::WebContentsObserver::Observe(NULL); 170 171 // Schedule the Viewer for deletion. Ensures distillation is cancelled, and 172 // any pending data stored in |buffer_| is released. 173 base::MessageLoop::current()->DeleteSoon(FROM_HERE, this); 174} 175 176void DomDistillerViewerSource::RequestViewerHandle::DidFinishLoad( 177 content::RenderFrameHost* render_frame_host, 178 const GURL& validated_url) { 179 if (render_frame_host->GetParent()) { 180 return; 181 } 182 waiting_for_page_ready_ = false; 183 if (buffer_.empty()) { 184 return; 185 } 186 web_contents()->GetMainFrame()->ExecuteJavaScript(base::UTF8ToUTF16(buffer_)); 187 buffer_.clear(); 188} 189 190void DomDistillerViewerSource::RequestViewerHandle::OnArticleReady( 191 const DistilledArticleProto* article_proto) { 192 if (page_count_ == 0) { 193 // This is a single-page article. 194 std::string unsafe_page_html = 195 viewer::GetUnsafeArticleHtml( 196 article_proto, 197 distilled_page_prefs_->GetTheme(), 198 distilled_page_prefs_->GetFontFamily()); 199 callback_.Run(base::RefCountedString::TakeString(&unsafe_page_html)); 200 } else if (page_count_ == article_proto->pages_size()) { 201 // We may still be showing the "Loading" indicator. 202 SendJavaScript(viewer::GetToggleLoadingIndicatorJs(true)); 203 } else { 204 // It's possible that we didn't get some incremental updates from the 205 // distiller. Ensure all remaining pages are flushed to the viewer. 206 for (;page_count_ < article_proto->pages_size(); page_count_++) { 207 const DistilledPageProto& page = article_proto->pages(page_count_); 208 SendJavaScript( 209 viewer::GetUnsafeIncrementalDistilledPageJs( 210 &page, 211 page_count_ == article_proto->pages_size())); 212 } 213 } 214 // No need to hold on to the ViewerHandle now that distillation is complete. 215 viewer_handle_.reset(); 216} 217 218void DomDistillerViewerSource::RequestViewerHandle::OnArticleUpdated( 219 ArticleDistillationUpdate article_update) { 220 for (;page_count_ < static_cast<int>(article_update.GetPagesSize()); 221 page_count_++) { 222 const DistilledPageProto& page = 223 article_update.GetDistilledPage(page_count_); 224 if (page_count_ == 0) { 225 // This is the first page, so send Viewer page scaffolding too. 226 std::string unsafe_page_html = viewer::GetUnsafePartialArticleHtml( 227 &page, 228 distilled_page_prefs_->GetTheme(), 229 distilled_page_prefs_->GetFontFamily()); 230 callback_.Run(base::RefCountedString::TakeString(&unsafe_page_html)); 231 } else { 232 SendJavaScript( 233 viewer::GetUnsafeIncrementalDistilledPageJs(&page, false)); 234 } 235 } 236} 237 238void DomDistillerViewerSource::RequestViewerHandle::TakeViewerHandle( 239 scoped_ptr<ViewerHandle> viewer_handle) { 240 viewer_handle_ = viewer_handle.Pass(); 241} 242 243void DomDistillerViewerSource::RequestViewerHandle::OnChangeTheme( 244 DistilledPagePrefs::Theme new_theme) { 245 SendJavaScript(viewer::GetDistilledPageThemeJs(new_theme)); 246} 247 248void DomDistillerViewerSource::RequestViewerHandle::OnChangeFontFamily( 249 DistilledPagePrefs::FontFamily new_font) { 250 SendJavaScript(viewer::GetDistilledPageFontFamilyJs(new_font)); 251} 252 253DomDistillerViewerSource::DomDistillerViewerSource( 254 DomDistillerServiceInterface* dom_distiller_service, 255 const std::string& scheme) 256 : scheme_(scheme), dom_distiller_service_(dom_distiller_service) { 257} 258 259DomDistillerViewerSource::~DomDistillerViewerSource() { 260} 261 262std::string DomDistillerViewerSource::GetSource() const { 263 return scheme_ + "://"; 264} 265 266void DomDistillerViewerSource::StartDataRequest( 267 const std::string& path, 268 int render_process_id, 269 int render_frame_id, 270 const content::URLDataSource::GotDataCallback& callback) { 271 content::RenderFrameHost* render_frame_host = 272 content::RenderFrameHost::FromID(render_process_id, render_frame_id); 273 if (!render_frame_host) return; 274 content::RenderViewHost* render_view_host = 275 render_frame_host->GetRenderViewHost(); 276 DCHECK(render_view_host); 277 CHECK_EQ(0, render_view_host->GetEnabledBindings()); 278 279 if (kViewerCssPath == path) { 280 std::string css = viewer::GetCss(); 281 callback.Run(base::RefCountedString::TakeString(&css)); 282 return; 283 } 284 if (kViewerJsPath == path) { 285 std::string js = viewer::GetJavaScript(); 286 callback.Run(base::RefCountedString::TakeString(&js)); 287 return; 288 } 289 if (kViewerViewOriginalPath == path) { 290 content::RecordAction(base::UserMetricsAction("DomDistiller_ViewOriginal")); 291 callback.Run(NULL); 292 return; 293 } 294 content::WebContents* web_contents = 295 content::WebContents::FromRenderFrameHost(render_frame_host); 296 DCHECK(web_contents); 297 // An empty |path| is invalid, but guard against it. If not empty, assume 298 // |path| starts with '?', which is stripped away. 299 const std::string path_after_query_separator = 300 path.size() > 0 ? path.substr(1) : ""; 301 RequestViewerHandle* request_viewer_handle = new RequestViewerHandle( 302 web_contents, scheme_, path_after_query_separator, callback, 303 dom_distiller_service_->GetDistilledPagePrefs()); 304 scoped_ptr<ViewerHandle> viewer_handle = viewer::CreateViewRequest( 305 dom_distiller_service_, path, request_viewer_handle, 306 web_contents->GetContainerBounds().size()); 307 308 if (viewer_handle) { 309 // The service returned a |ViewerHandle| and guarantees it will call 310 // the |RequestViewerHandle|, so passing ownership to it, to ensure the 311 // request is not cancelled. The |RequestViewerHandle| will delete itself 312 // after receiving the callback. 313 request_viewer_handle->TakeViewerHandle(viewer_handle.Pass()); 314 } else { 315 // The service did not return a |ViewerHandle|, which means the 316 // |RequestViewerHandle| will never be called, so clean up now. 317 delete request_viewer_handle; 318 319 std::string error_page_html = viewer::GetErrorPageHtml( 320 dom_distiller_service_->GetDistilledPagePrefs()->GetTheme(), 321 dom_distiller_service_->GetDistilledPagePrefs()->GetFontFamily()); 322 callback.Run(base::RefCountedString::TakeString(&error_page_html)); 323 } 324}; 325 326std::string DomDistillerViewerSource::GetMimeType( 327 const std::string& path) const { 328 if (kViewerCssPath == path) { 329 return "text/css"; 330 } 331 if (kViewerJsPath == path) { 332 return "text/javascript"; 333 } 334 return "text/html"; 335} 336 337bool DomDistillerViewerSource::ShouldServiceRequest( 338 const net::URLRequest* request) const { 339 return request->url().SchemeIs(scheme_.c_str()); 340} 341 342// TODO(nyquist): Start tracking requests using this method. 343void DomDistillerViewerSource::WillServiceRequest( 344 const net::URLRequest* request, 345 std::string* path) const { 346} 347 348std::string DomDistillerViewerSource::GetContentSecurityPolicyObjectSrc() 349 const { 350 return "object-src 'none'; style-src 'self' https://fonts.googleapis.com;"; 351} 352 353} // namespace dom_distiller 354