| // Copyright 2014 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 "components/dom_distiller/content/dom_distiller_viewer_source.h" |
| |
| #include <sstream> |
| #include <string> |
| #include <vector> |
| |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/dom_distiller/core/task_tracker.h" |
| #include "components/dom_distiller/core/url_constants.h" |
| #include "components/dom_distiller/core/viewer.h" |
| #include "content/public/browser/navigation_details.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "net/base/url_util.h" |
| #include "net/url_request/url_request.h" |
| |
| namespace dom_distiller { |
| |
| // Handles receiving data asynchronously for a specific entry, and passing |
| // it along to the data callback for the data source. Lifetime matches that of |
| // the current main frame's page in the Viewer instance. |
| class DomDistillerViewerSource::RequestViewerHandle |
| : public ViewRequestDelegate, |
| public content::WebContentsObserver { |
| public: |
| explicit RequestViewerHandle( |
| content::WebContents* web_contents, |
| const std::string& expected_scheme, |
| const std::string& expected_request_path, |
| const content::URLDataSource::GotDataCallback& callback); |
| virtual ~RequestViewerHandle(); |
| |
| // ViewRequestDelegate implementation. |
| virtual void OnArticleReady( |
| const DistilledArticleProto* article_proto) OVERRIDE; |
| |
| virtual void OnArticleUpdated( |
| ArticleDistillationUpdate article_update) OVERRIDE; |
| |
| void TakeViewerHandle(scoped_ptr<ViewerHandle> viewer_handle); |
| |
| // WebContentsObserver: |
| virtual void DidNavigateMainFrame( |
| const content::LoadCommittedDetails& details, |
| const content::FrameNavigateParams& params) OVERRIDE; |
| virtual void RenderProcessGone(base::TerminationStatus status) OVERRIDE; |
| virtual void WebContentsDestroyed() OVERRIDE; |
| virtual void DidFinishLoad( |
| int64 frame_id, |
| const GURL& validated_url, |
| bool is_main_frame, |
| content::RenderViewHost* render_view_host) OVERRIDE; |
| |
| private: |
| // Sends JavaScript to the attached Viewer, buffering data if the viewer isn't |
| // ready. |
| void SendJavaScript(const std::string& buffer); |
| |
| // Cancels the current view request. Once called, no updates will be |
| // propagated to the view, and the request to DomDistillerService will be |
| // cancelled. |
| void Cancel(); |
| |
| // The handle to the view request towards the DomDistillerService. It |
| // needs to be kept around to ensure the distillation request finishes. |
| scoped_ptr<ViewerHandle> viewer_handle_; |
| |
| // WebContents associated with the Viewer's render process. |
| content::WebContents* web_contents_; |
| |
| // The scheme hosting the current view request; |
| std::string expected_scheme_; |
| |
| // The query path for the current view request. |
| std::string expected_request_path_; |
| |
| // Holds the callback to where the data retrieved is sent back. |
| content::URLDataSource::GotDataCallback callback_; |
| |
| // Number of pages of the distilled article content that have been rendered by |
| // the viewer. |
| int page_count_; |
| |
| // Whether the page is sufficiently initialized to handle updates from the |
| // distiller. |
| bool waiting_for_page_ready_; |
| |
| // Temporary store of pending JavaScript if the page isn't ready to receive |
| // data from distillation. |
| std::string buffer_; |
| }; |
| |
| DomDistillerViewerSource::RequestViewerHandle::RequestViewerHandle( |
| content::WebContents* web_contents, |
| const std::string& expected_scheme, |
| const std::string& expected_request_path, |
| const content::URLDataSource::GotDataCallback& callback) |
| : web_contents_(web_contents), |
| expected_scheme_(expected_scheme), |
| expected_request_path_(expected_request_path), |
| callback_(callback), |
| page_count_(0), |
| waiting_for_page_ready_(true) { |
| content::WebContentsObserver::Observe(web_contents_); |
| } |
| |
| DomDistillerViewerSource::RequestViewerHandle::~RequestViewerHandle() { |
| // Balanced with constructor although can be a no-op if frame navigated away. |
| content::WebContentsObserver::Observe(NULL); |
| } |
| |
| void DomDistillerViewerSource::RequestViewerHandle::SendJavaScript( |
| const std::string& buffer) { |
| if (waiting_for_page_ready_) { |
| buffer_ += buffer; |
| } else { |
| if (web_contents_) { |
| web_contents_->GetMainFrame()->ExecuteJavaScript( |
| base::UTF8ToUTF16(buffer)); |
| } |
| } |
| } |
| |
| void DomDistillerViewerSource::RequestViewerHandle::DidNavigateMainFrame( |
| const content::LoadCommittedDetails& details, |
| const content::FrameNavigateParams& params) { |
| const GURL& navigation = details.entry->GetURL(); |
| if (details.is_in_page || ( |
| navigation.SchemeIs(expected_scheme_.c_str()) && |
| expected_request_path_ == navigation.query())) { |
| // In-page navigations, as well as the main view request can be ignored. |
| return; |
| } |
| |
| Cancel(); |
| |
| } |
| |
| void DomDistillerViewerSource::RequestViewerHandle::RenderProcessGone( |
| base::TerminationStatus status) { |
| Cancel(); |
| } |
| |
| void DomDistillerViewerSource::RequestViewerHandle::WebContentsDestroyed() { |
| Cancel(); |
| } |
| |
| void DomDistillerViewerSource::RequestViewerHandle::Cancel() { |
| // Ensure we don't send any incremental updates to the Viewer. |
| web_contents_ = NULL; |
| |
| // No need to listen for notifications. |
| content::WebContentsObserver::Observe(NULL); |
| |
| // Schedule the Viewer for deletion. Ensures distillation is cancelled, and |
| // any pending data stored in |buffer_| is released. |
| base::MessageLoop::current()->DeleteSoon(FROM_HERE, this); |
| } |
| |
| void DomDistillerViewerSource::RequestViewerHandle::DidFinishLoad( |
| int64 frame_id, |
| const GURL& validated_url, |
| bool is_main_frame, |
| content::RenderViewHost* render_view_host) { |
| if (!is_main_frame || web_contents_ == NULL) { |
| return; |
| } |
| waiting_for_page_ready_ = false; |
| if (buffer_.empty()) { |
| return; |
| } |
| if (web_contents_) { |
| web_contents_->GetMainFrame()->ExecuteJavaScript( |
| base::UTF8ToUTF16(buffer_)); |
| } |
| buffer_.clear(); |
| } |
| |
| void DomDistillerViewerSource::RequestViewerHandle::OnArticleReady( |
| const DistilledArticleProto* article_proto) { |
| if (page_count_ == 0) { |
| // This is a single-page article. |
| std::string unsafe_page_html = viewer::GetUnsafeArticleHtml(article_proto); |
| callback_.Run(base::RefCountedString::TakeString(&unsafe_page_html)); |
| } else if (page_count_ == article_proto->pages_size()) { |
| // We may still be showing the "Loading" indicator. |
| SendJavaScript(viewer::GetToggleLoadingIndicatorJs(true)); |
| } else { |
| // It's possible that we didn't get some incremental updates from the |
| // distiller. Ensure all remaining pages are flushed to the viewer. |
| for (;page_count_ < article_proto->pages_size(); page_count_++) { |
| const DistilledPageProto& page = article_proto->pages(page_count_); |
| SendJavaScript( |
| viewer::GetUnsafeIncrementalDistilledPageJs( |
| &page, |
| page_count_ == article_proto->pages_size())); |
| } |
| } |
| // No need to hold on to the ViewerHandle now that distillation is complete. |
| viewer_handle_.reset(); |
| } |
| |
| void DomDistillerViewerSource::RequestViewerHandle::OnArticleUpdated( |
| ArticleDistillationUpdate article_update) { |
| for (;page_count_ < static_cast<int>(article_update.GetPagesSize()); |
| page_count_++) { |
| const DistilledPageProto& page = |
| article_update.GetDistilledPage(page_count_); |
| if (page_count_ == 0) { |
| // This is the first page, so send Viewer page scaffolding too. |
| std::string unsafe_page_html = viewer::GetUnsafePartialArticleHtml(&page); |
| callback_.Run(base::RefCountedString::TakeString(&unsafe_page_html)); |
| } else { |
| SendJavaScript( |
| viewer::GetUnsafeIncrementalDistilledPageJs(&page, false)); |
| } |
| } |
| } |
| |
| void DomDistillerViewerSource::RequestViewerHandle::TakeViewerHandle( |
| scoped_ptr<ViewerHandle> viewer_handle) { |
| viewer_handle_ = viewer_handle.Pass(); |
| } |
| |
| DomDistillerViewerSource::DomDistillerViewerSource( |
| DomDistillerServiceInterface* dom_distiller_service, |
| const std::string& scheme) |
| : scheme_(scheme), dom_distiller_service_(dom_distiller_service) { |
| } |
| |
| DomDistillerViewerSource::~DomDistillerViewerSource() { |
| } |
| |
| std::string DomDistillerViewerSource::GetSource() const { |
| return scheme_ + "://"; |
| } |
| |
| void DomDistillerViewerSource::StartDataRequest( |
| const std::string& path, |
| int render_process_id, |
| int render_frame_id, |
| const content::URLDataSource::GotDataCallback& callback) { |
| content::RenderFrameHost* render_frame_host = |
| content::RenderFrameHost::FromID(render_process_id, render_frame_id); |
| DCHECK(render_frame_host); |
| content::RenderViewHost* render_view_host = |
| render_frame_host->GetRenderViewHost(); |
| DCHECK(render_view_host); |
| CHECK_EQ(0, render_view_host->GetEnabledBindings()); |
| |
| if (kViewerCssPath == path) { |
| std::string css = viewer::GetCss(); |
| callback.Run(base::RefCountedString::TakeString(&css)); |
| return; |
| } |
| if (kViewerJsPath == path) { |
| std::string js = viewer::GetJavaScript(); |
| callback.Run(base::RefCountedString::TakeString(&js)); |
| return; |
| } |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost( |
| content::RenderFrameHost::FromID(render_process_id, |
| render_frame_id)); |
| DCHECK(web_contents); |
| // An empty |path| is invalid, but guard against it. If not empty, assume |
| // |path| starts with '?', which is stripped away. |
| const std::string path_after_query_separator = |
| path.size() > 0 ? path.substr(1) : ""; |
| RequestViewerHandle* request_viewer_handle = new RequestViewerHandle( |
| web_contents, scheme_, path_after_query_separator, callback); |
| scoped_ptr<ViewerHandle> viewer_handle = viewer::CreateViewRequest( |
| dom_distiller_service_, path, request_viewer_handle); |
| |
| if (viewer_handle) { |
| // The service returned a |ViewerHandle| and guarantees it will call |
| // the |RequestViewerHandle|, so passing ownership to it, to ensure the |
| // request is not cancelled. The |RequestViewerHandle| will delete itself |
| // after receiving the callback. |
| request_viewer_handle->TakeViewerHandle(viewer_handle.Pass()); |
| } else { |
| // The service did not return a |ViewerHandle|, which means the |
| // |RequestViewerHandle| will never be called, so clean up now. |
| delete request_viewer_handle; |
| |
| std::string error_page_html = viewer::GetErrorPageHtml(); |
| callback.Run(base::RefCountedString::TakeString(&error_page_html)); |
| } |
| }; |
| |
| std::string DomDistillerViewerSource::GetMimeType( |
| const std::string& path) const { |
| if (kViewerCssPath == path) { |
| return "text/css"; |
| } |
| if (kViewerJsPath == path) { |
| return "text/javascript"; |
| } |
| return "text/html"; |
| } |
| |
| bool DomDistillerViewerSource::ShouldServiceRequest( |
| const net::URLRequest* request) const { |
| return request->url().SchemeIs(scheme_.c_str()); |
| } |
| |
| // TODO(nyquist): Start tracking requests using this method. |
| void DomDistillerViewerSource::WillServiceRequest( |
| const net::URLRequest* request, |
| std::string* path) const { |
| } |
| |
| std::string DomDistillerViewerSource::GetContentSecurityPolicyObjectSrc() |
| const { |
| return "object-src 'none'; style-src 'self';"; |
| } |
| |
| } // namespace dom_distiller |