Basic hookup for distiller feedback
[chromium-blink-merge.git] / components / dom_distiller / content / dom_distiller_viewer_source.cc
blobb478ce2212d63c471d24d39a2f507301abbcb8ce
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"
7 #include <sstream>
8 #include <string>
9 #include <vector>
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/feedback_reporter.h"
19 #include "components/dom_distiller/core/task_tracker.h"
20 #include "components/dom_distiller/core/url_constants.h"
21 #include "components/dom_distiller/core/viewer.h"
22 #include "content/public/browser/navigation_details.h"
23 #include "content/public/browser/navigation_entry.h"
24 #include "content/public/browser/render_frame_host.h"
25 #include "content/public/browser/render_view_host.h"
26 #include "content/public/browser/user_metrics.h"
27 #include "content/public/browser/web_contents.h"
28 #include "content/public/browser/web_contents_observer.h"
29 #include "net/base/url_util.h"
30 #include "net/url_request/url_request.h"
32 namespace dom_distiller {
34 // Handles receiving data asynchronously for a specific entry, and passing
35 // it along to the data callback for the data source. Lifetime matches that of
36 // the current main frame's page in the Viewer instance.
37 class DomDistillerViewerSource::RequestViewerHandle
38 : public ViewRequestDelegate,
39 public content::WebContentsObserver,
40 public DistilledPagePrefs::Observer {
41 public:
42 explicit RequestViewerHandle(
43 content::WebContents* web_contents,
44 const std::string& expected_scheme,
45 const std::string& expected_request_path,
46 const content::URLDataSource::GotDataCallback& callback,
47 DistilledPagePrefs* distilled_page_prefs);
48 ~RequestViewerHandle() override;
50 // Flag this request as an error and send the error page template.
51 void flagAsErrorPage();
53 // ViewRequestDelegate implementation:
54 void OnArticleReady(const DistilledArticleProto* article_proto) override;
56 void OnArticleUpdated(ArticleDistillationUpdate article_update) override;
58 void TakeViewerHandle(scoped_ptr<ViewerHandle> viewer_handle);
60 // content::WebContentsObserver implementation:
61 void DidNavigateMainFrame(
62 const content::LoadCommittedDetails& details,
63 const content::FrameNavigateParams& params) override;
64 void RenderProcessGone(base::TerminationStatus status) override;
65 void WebContentsDestroyed() override;
66 void DidFinishLoad(content::RenderFrameHost* render_frame_host,
67 const GURL& validated_url) override;
69 private:
70 // Sends JavaScript to the attached Viewer, buffering data if the viewer isn't
71 // ready.
72 void SendJavaScript(const std::string& buffer);
74 // Cancels the current view request. Once called, no updates will be
75 // propagated to the view, and the request to DomDistillerService will be
76 // cancelled.
77 void Cancel();
79 // DistilledPagePrefs::Observer implementation:
80 void OnChangeFontFamily(
81 DistilledPagePrefs::FontFamily new_font_family) override;
82 void OnChangeTheme(DistilledPagePrefs::Theme new_theme) override;
84 // The handle to the view request towards the DomDistillerService. It
85 // needs to be kept around to ensure the distillation request finishes.
86 scoped_ptr<ViewerHandle> viewer_handle_;
88 // The scheme hosting the current view request;
89 std::string expected_scheme_;
91 // The query path for the current view request.
92 std::string expected_request_path_;
94 // Holds the callback to where the data retrieved is sent back.
95 content::URLDataSource::GotDataCallback callback_;
97 // Number of pages of the distilled article content that have been rendered by
98 // the viewer.
99 int page_count_;
101 // Interface for accessing preferences for distilled pages.
102 DistilledPagePrefs* distilled_page_prefs_;
104 // Whether the page is sufficiently initialized to handle updates from the
105 // distiller.
106 bool waiting_for_page_ready_;
108 // Temporary store of pending JavaScript if the page isn't ready to receive
109 // data from distillation.
110 std::string buffer_;
112 // Flag to tell this observer that the web contents are in an error state.
113 bool is_error_page_;
116 DomDistillerViewerSource::RequestViewerHandle::RequestViewerHandle(
117 content::WebContents* web_contents,
118 const std::string& expected_scheme,
119 const std::string& expected_request_path,
120 const content::URLDataSource::GotDataCallback& callback,
121 DistilledPagePrefs* distilled_page_prefs)
122 : expected_scheme_(expected_scheme),
123 expected_request_path_(expected_request_path),
124 callback_(callback),
125 page_count_(0),
126 distilled_page_prefs_(distilled_page_prefs),
127 waiting_for_page_ready_(true),
128 is_error_page_(false) {
129 content::WebContentsObserver::Observe(web_contents);
130 distilled_page_prefs_->AddObserver(this);
133 DomDistillerViewerSource::RequestViewerHandle::~RequestViewerHandle() {
134 distilled_page_prefs_->RemoveObserver(this);
137 void DomDistillerViewerSource::RequestViewerHandle::flagAsErrorPage() {
138 is_error_page_ = true;
139 std::string error_page_html = viewer::GetErrorPageHtml(
140 distilled_page_prefs_->GetTheme(),
141 distilled_page_prefs_->GetFontFamily());
142 callback_.Run(base::RefCountedString::TakeString(&error_page_html));
145 void DomDistillerViewerSource::RequestViewerHandle::SendJavaScript(
146 const std::string& buffer) {
147 if (waiting_for_page_ready_) {
148 buffer_ += buffer;
149 } else {
150 if (web_contents()) {
151 web_contents()->GetMainFrame()->ExecuteJavaScript(
152 base::UTF8ToUTF16(buffer));
157 void DomDistillerViewerSource::RequestViewerHandle::DidNavigateMainFrame(
158 const content::LoadCommittedDetails& details,
159 const content::FrameNavigateParams& params) {
160 const GURL& navigation = details.entry->GetURL();
161 if (details.is_in_page || (
162 navigation.SchemeIs(expected_scheme_.c_str()) &&
163 expected_request_path_ == navigation.query())) {
164 // In-page navigations, as well as the main view request can be ignored.
165 return;
168 Cancel();
171 void DomDistillerViewerSource::RequestViewerHandle::RenderProcessGone(
172 base::TerminationStatus status) {
173 Cancel();
176 void DomDistillerViewerSource::RequestViewerHandle::WebContentsDestroyed() {
177 Cancel();
180 void DomDistillerViewerSource::RequestViewerHandle::Cancel() {
181 // No need to listen for notifications.
182 content::WebContentsObserver::Observe(NULL);
184 // Schedule the Viewer for deletion. Ensures distillation is cancelled, and
185 // any pending data stored in |buffer_| is released.
186 base::MessageLoop::current()->DeleteSoon(FROM_HERE, this);
189 void DomDistillerViewerSource::RequestViewerHandle::DidFinishLoad(
190 content::RenderFrameHost* render_frame_host,
191 const GURL& validated_url) {
192 if (is_error_page_) {
193 waiting_for_page_ready_ = false;
194 SendJavaScript(viewer::GetErrorPageJs());
195 SendJavaScript(viewer::GetShowFeedbackFormJs());
196 Cancel(); // This will cause the object to clean itself up.
197 return;
200 if (render_frame_host->GetParent()) {
201 return;
203 waiting_for_page_ready_ = false;
204 if (buffer_.empty()) {
205 return;
207 web_contents()->GetMainFrame()->ExecuteJavaScript(base::UTF8ToUTF16(buffer_));
208 buffer_.clear();
211 void DomDistillerViewerSource::RequestViewerHandle::OnArticleReady(
212 const DistilledArticleProto* article_proto) {
213 // TODO(mdjones): Move this logic to super class so it can be used in both
214 // android and IOS. http://crbug.com/472797
215 if (page_count_ == 0) {
216 std::string unsafe_page_html = viewer::GetUnsafeArticleTemplateHtml(
217 &article_proto->pages(0),
218 distilled_page_prefs_->GetTheme(),
219 distilled_page_prefs_->GetFontFamily());
220 callback_.Run(base::RefCountedString::TakeString(&unsafe_page_html));
221 // Send first page to client.
222 SendJavaScript(viewer::GetUnsafeArticleContentJs(article_proto));
223 // If any content was loaded, show the feedback form.
224 SendJavaScript(viewer::GetShowFeedbackFormJs());
225 } else if (page_count_ == article_proto->pages_size()) {
226 // We may still be showing the "Loading" indicator.
227 SendJavaScript(viewer::GetToggleLoadingIndicatorJs(true));
228 } else {
229 // It's possible that we didn't get some incremental updates from the
230 // distiller. Ensure all remaining pages are flushed to the viewer.
231 for (;page_count_ < article_proto->pages_size(); page_count_++) {
232 const DistilledPageProto& page = article_proto->pages(page_count_);
233 SendJavaScript(
234 viewer::GetUnsafeIncrementalDistilledPageJs(
235 &page,
236 page_count_ == article_proto->pages_size()));
239 // No need to hold on to the ViewerHandle now that distillation is complete.
240 viewer_handle_.reset();
243 void DomDistillerViewerSource::RequestViewerHandle::OnArticleUpdated(
244 ArticleDistillationUpdate article_update) {
245 for (;page_count_ < static_cast<int>(article_update.GetPagesSize());
246 page_count_++) {
247 const DistilledPageProto& page =
248 article_update.GetDistilledPage(page_count_);
249 // Send the page content to the client. This will execute after the page is
250 // ready.
251 SendJavaScript(viewer::GetUnsafeIncrementalDistilledPageJs(&page, false));
253 if (page_count_ == 0) {
254 // This is the first page, so send Viewer page scaffolding too.
255 std::string unsafe_page_html = viewer::GetUnsafeArticleTemplateHtml(
256 &page,
257 distilled_page_prefs_->GetTheme(),
258 distilled_page_prefs_->GetFontFamily());
259 callback_.Run(base::RefCountedString::TakeString(&unsafe_page_html));
260 // If any content was loaded, show the feedback form.
261 SendJavaScript(viewer::GetShowFeedbackFormJs());
266 void DomDistillerViewerSource::RequestViewerHandle::TakeViewerHandle(
267 scoped_ptr<ViewerHandle> viewer_handle) {
268 viewer_handle_ = viewer_handle.Pass();
271 void DomDistillerViewerSource::RequestViewerHandle::OnChangeTheme(
272 DistilledPagePrefs::Theme new_theme) {
273 SendJavaScript(viewer::GetDistilledPageThemeJs(new_theme));
276 void DomDistillerViewerSource::RequestViewerHandle::OnChangeFontFamily(
277 DistilledPagePrefs::FontFamily new_font) {
278 SendJavaScript(viewer::GetDistilledPageFontFamilyJs(new_font));
281 DomDistillerViewerSource::DomDistillerViewerSource(
282 DomDistillerServiceInterface* dom_distiller_service,
283 const std::string& scheme)
284 : scheme_(scheme), dom_distiller_service_(dom_distiller_service) {
287 DomDistillerViewerSource::~DomDistillerViewerSource() {
290 std::string DomDistillerViewerSource::GetSource() const {
291 return scheme_ + "://";
294 void DomDistillerViewerSource::StartDataRequest(
295 const std::string& path,
296 int render_process_id,
297 int render_frame_id,
298 const content::URLDataSource::GotDataCallback& callback) {
299 content::RenderFrameHost* render_frame_host =
300 content::RenderFrameHost::FromID(render_process_id, render_frame_id);
301 if (!render_frame_host) return;
302 content::RenderViewHost* render_view_host =
303 render_frame_host->GetRenderViewHost();
304 DCHECK(render_view_host);
305 CHECK_EQ(0, render_view_host->GetEnabledBindings());
307 if (kViewerCssPath == path) {
308 std::string css = viewer::GetCss();
309 callback.Run(base::RefCountedString::TakeString(&css));
310 return;
311 } else if (kViewerJsPath == path) {
312 std::string js = viewer::GetJavaScript();
313 callback.Run(base::RefCountedString::TakeString(&js));
314 return;
315 } else if (kViewerViewOriginalPath == path) {
316 content::RecordAction(base::UserMetricsAction("DomDistiller_ViewOriginal"));
317 callback.Run(NULL);
318 return;
319 } else if (kFeedbackBad == path) {
320 FeedbackReporter::ReportQuality(false);
321 return;
322 } else if (kFeedbackGood == path) {
323 FeedbackReporter::ReportQuality(true);
324 return;
326 content::WebContents* web_contents =
327 content::WebContents::FromRenderFrameHost(render_frame_host);
328 DCHECK(web_contents);
329 // An empty |path| is invalid, but guard against it. If not empty, assume
330 // |path| starts with '?', which is stripped away.
331 const std::string path_after_query_separator =
332 path.size() > 0 ? path.substr(1) : "";
333 RequestViewerHandle* request_viewer_handle = new RequestViewerHandle(
334 web_contents, scheme_, path_after_query_separator, callback,
335 dom_distiller_service_->GetDistilledPagePrefs());
336 scoped_ptr<ViewerHandle> viewer_handle = viewer::CreateViewRequest(
337 dom_distiller_service_, path, request_viewer_handle,
338 web_contents->GetContainerBounds().size());
340 if (viewer_handle) {
341 // The service returned a |ViewerHandle| and guarantees it will call
342 // the |RequestViewerHandle|, so passing ownership to it, to ensure the
343 // request is not cancelled. The |RequestViewerHandle| will delete itself
344 // after receiving the callback.
345 request_viewer_handle->TakeViewerHandle(viewer_handle.Pass());
346 } else {
347 request_viewer_handle->flagAsErrorPage();
351 std::string DomDistillerViewerSource::GetMimeType(
352 const std::string& path) const {
353 if (kViewerCssPath == path) {
354 return "text/css";
356 if (kViewerJsPath == path) {
357 return "text/javascript";
359 return "text/html";
362 bool DomDistillerViewerSource::ShouldServiceRequest(
363 const net::URLRequest* request) const {
364 return request->url().SchemeIs(scheme_.c_str());
367 // TODO(nyquist): Start tracking requests using this method.
368 void DomDistillerViewerSource::WillServiceRequest(
369 const net::URLRequest* request,
370 std::string* path) const {
373 std::string DomDistillerViewerSource::GetContentSecurityPolicyObjectSrc()
374 const {
375 return "object-src 'none'; style-src 'self' https://fonts.googleapis.com;";
378 } // namespace dom_distiller