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.
5 #include "components/dom_distiller/content/dom_distiller_viewer_source.h"
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"
31 namespace dom_distiller {
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.
36 class DomDistillerViewerSource::RequestViewerHandle
37 : public ViewRequestDelegate,
38 public content::WebContentsObserver,
39 public DistilledPagePrefs::Observer {
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();
49 // ViewRequestDelegate implementation:
50 virtual void OnArticleReady(
51 const DistilledArticleProto* article_proto) OVERRIDE;
53 virtual void OnArticleUpdated(
54 ArticleDistillationUpdate article_update) OVERRIDE;
56 void TakeViewerHandle(scoped_ptr<ViewerHandle> viewer_handle);
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;
68 // Sends JavaScript to the attached Viewer, buffering data if the viewer isn't
70 void SendJavaScript(const std::string& buffer);
72 // Cancels the current view request. Once called, no updates will be
73 // propagated to the view, and the request to DomDistillerService will be
77 // DistilledPagePrefs::Observer implementation:
78 virtual void OnChangeFontFamily(
79 DistilledPagePrefs::FontFamily new_font_family) OVERRIDE;
80 virtual void OnChangeTheme(DistilledPagePrefs::Theme new_theme) OVERRIDE;
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_;
86 // The scheme hosting the current view request;
87 std::string expected_scheme_;
89 // The query path for the current view request.
90 std::string expected_request_path_;
92 // Holds the callback to where the data retrieved is sent back.
93 content::URLDataSource::GotDataCallback callback_;
95 // Number of pages of the distilled article content that have been rendered by
99 // Interface for accessing preferences for distilled pages.
100 DistilledPagePrefs* distilled_page_prefs_;
102 // Whether the page is sufficiently initialized to handle updates from the
104 bool waiting_for_page_ready_;
106 // Temporary store of pending JavaScript if the page isn't ready to receive
107 // data from distillation.
111 DomDistillerViewerSource::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),
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);
127 DomDistillerViewerSource::RequestViewerHandle::~RequestViewerHandle() {
128 distilled_page_prefs_->RemoveObserver(this);
131 void DomDistillerViewerSource::RequestViewerHandle::SendJavaScript(
132 const std::string& buffer) {
133 if (waiting_for_page_ready_) {
136 if (web_contents()) {
137 web_contents()->GetMainFrame()->ExecuteJavaScript(
138 base::UTF8ToUTF16(buffer));
143 void 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.
158 void DomDistillerViewerSource::RequestViewerHandle::RenderProcessGone(
159 base::TerminationStatus status) {
163 void DomDistillerViewerSource::RequestViewerHandle::WebContentsDestroyed() {
167 void DomDistillerViewerSource::RequestViewerHandle::Cancel() {
168 // No need to listen for notifications.
169 content::WebContentsObserver::Observe(NULL);
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);
176 void DomDistillerViewerSource::RequestViewerHandle::DidFinishLoad(
177 content::RenderFrameHost* render_frame_host,
178 const GURL& validated_url) {
179 if (render_frame_host->GetParent()) {
182 waiting_for_page_ready_ = false;
183 if (buffer_.empty()) {
186 web_contents()->GetMainFrame()->ExecuteJavaScript(base::UTF8ToUTF16(buffer_));
190 void 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(
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));
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_);
209 viewer::GetUnsafeIncrementalDistilledPageJs(
211 page_count_ == article_proto->pages_size()));
214 // No need to hold on to the ViewerHandle now that distillation is complete.
215 viewer_handle_.reset();
218 void DomDistillerViewerSource::RequestViewerHandle::OnArticleUpdated(
219 ArticleDistillationUpdate article_update) {
220 for (;page_count_ < static_cast<int>(article_update.GetPagesSize());
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(
228 distilled_page_prefs_->GetTheme(),
229 distilled_page_prefs_->GetFontFamily());
230 callback_.Run(base::RefCountedString::TakeString(&unsafe_page_html));
233 viewer::GetUnsafeIncrementalDistilledPageJs(&page, false));
238 void DomDistillerViewerSource::RequestViewerHandle::TakeViewerHandle(
239 scoped_ptr<ViewerHandle> viewer_handle) {
240 viewer_handle_ = viewer_handle.Pass();
243 void DomDistillerViewerSource::RequestViewerHandle::OnChangeTheme(
244 DistilledPagePrefs::Theme new_theme) {
245 SendJavaScript(viewer::GetDistilledPageThemeJs(new_theme));
248 void DomDistillerViewerSource::RequestViewerHandle::OnChangeFontFamily(
249 DistilledPagePrefs::FontFamily new_font) {
250 SendJavaScript(viewer::GetDistilledPageFontFamilyJs(new_font));
253 DomDistillerViewerSource::DomDistillerViewerSource(
254 DomDistillerServiceInterface* dom_distiller_service,
255 const std::string& scheme)
256 : scheme_(scheme), dom_distiller_service_(dom_distiller_service) {
259 DomDistillerViewerSource::~DomDistillerViewerSource() {
262 std::string DomDistillerViewerSource::GetSource() const {
263 return scheme_ + "://";
266 void DomDistillerViewerSource::StartDataRequest(
267 const std::string& path,
268 int render_process_id,
270 const content::URLDataSource::GotDataCallback& callback) {
271 content::RenderFrameHost* render_frame_host =
272 content::RenderFrameHost::FromID(render_process_id, render_frame_id);
273 DCHECK(render_frame_host);
274 content::RenderViewHost* render_view_host =
275 render_frame_host->GetRenderViewHost();
276 DCHECK(render_view_host);
277 CHECK_EQ(0, render_view_host->GetEnabledBindings());
279 if (kViewerCssPath == path) {
280 std::string css = viewer::GetCss();
281 callback.Run(base::RefCountedString::TakeString(&css));
284 if (kViewerJsPath == path) {
285 std::string js = viewer::GetJavaScript();
286 callback.Run(base::RefCountedString::TakeString(&js));
289 if (kViewerViewOriginalPath == path) {
290 content::RecordAction(base::UserMetricsAction("DomDistiller_ViewOriginal"));
294 content::WebContents* web_contents =
295 content::WebContents::FromRenderFrameHost(
296 content::RenderFrameHost::FromID(render_process_id,
298 DCHECK(web_contents);
299 // An empty |path| is invalid, but guard against it. If not empty, assume
300 // |path| starts with '?', which is stripped away.
301 const std::string path_after_query_separator =
302 path.size() > 0 ? path.substr(1) : "";
303 RequestViewerHandle* request_viewer_handle = new RequestViewerHandle(
304 web_contents, scheme_, path_after_query_separator, callback,
305 dom_distiller_service_->GetDistilledPagePrefs());
306 scoped_ptr<ViewerHandle> viewer_handle = viewer::CreateViewRequest(
307 dom_distiller_service_, path, request_viewer_handle,
308 web_contents->GetContainerBounds().size());
311 // The service returned a |ViewerHandle| and guarantees it will call
312 // the |RequestViewerHandle|, so passing ownership to it, to ensure the
313 // request is not cancelled. The |RequestViewerHandle| will delete itself
314 // after receiving the callback.
315 request_viewer_handle->TakeViewerHandle(viewer_handle.Pass());
317 // The service did not return a |ViewerHandle|, which means the
318 // |RequestViewerHandle| will never be called, so clean up now.
319 delete request_viewer_handle;
321 std::string error_page_html = viewer::GetErrorPageHtml(
322 dom_distiller_service_->GetDistilledPagePrefs()->GetTheme(),
323 dom_distiller_service_->GetDistilledPagePrefs()->GetFontFamily());
324 callback.Run(base::RefCountedString::TakeString(&error_page_html));
328 std::string DomDistillerViewerSource::GetMimeType(
329 const std::string& path) const {
330 if (kViewerCssPath == path) {
333 if (kViewerJsPath == path) {
334 return "text/javascript";
339 bool DomDistillerViewerSource::ShouldServiceRequest(
340 const net::URLRequest* request) const {
341 return request->url().SchemeIs(scheme_.c_str());
344 // TODO(nyquist): Start tracking requests using this method.
345 void DomDistillerViewerSource::WillServiceRequest(
346 const net::URLRequest* request,
347 std::string* path) const {
350 std::string DomDistillerViewerSource::GetContentSecurityPolicyObjectSrc()
352 return "object-src 'none'; style-src 'self' https://fonts.googleapis.com;";
355 } // namespace dom_distiller