File: dom_distiller_viewer_source.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (312 lines) | stat: -rw-r--r-- 12,395 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
// Copyright 2014 The Chromium Authors
// 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/browser/dom_distiller_viewer_source.h"

#include <deque>
#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/ref_counted_memory.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "components/back_forward_cache/back_forward_cache_disable.h"
#include "components/dom_distiller/content/browser/distiller_javascript_utils.h"
#include "components/dom_distiller/core/distilled_page_prefs.h"
#include "components/dom_distiller/core/dom_distiller_request_view_base.h"
#include "components/dom_distiller/core/dom_distiller_service.h"
#include "components/dom_distiller/core/experiments.h"
#include "components/dom_distiller/core/task_tracker.h"
#include "components/dom_distiller/core/url_constants.h"
#include "components/dom_distiller/core/url_utils.h"
#include "components/dom_distiller/core/viewer.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.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 "mojo/public/cpp/bindings/remote.h"
#include "net/base/url_util.h"
#include "services/network/public/mojom/content_security_policy.mojom.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "ui/base/l10n/l10n_util.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 DomDistillerRequestViewBase,
      public content::WebContentsObserver {
 public:
  RequestViewerHandle(content::WebContents* web_contents,
                      const GURL& expected_url,
                      DistilledPagePrefs* distilled_page_prefs);
  ~RequestViewerHandle() override;

  // content::WebContentsObserver implementation:
  void DidFinishNavigation(
      content::NavigationHandle* navigation_handle) override;
  void PrimaryMainFrameRenderProcessGone(
      base::TerminationStatus status) override;
  void WebContentsDestroyed() override;
  void DOMContentLoaded(content::RenderFrameHost* render_frame_host) override;

 private:
  // Sends JavaScript to the attached Viewer, buffering data if the viewer isn't
  // ready.
  void SendJavaScript(const std::string& buffer) override;

  // 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 URL hosting the current view request;
  const GURL expected_url_;

  // 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::deque<std::string> buffers_;
};

DomDistillerViewerSource::RequestViewerHandle::RequestViewerHandle(
    content::WebContents* web_contents,
    const GURL& expected_url,
    DistilledPagePrefs* distilled_page_prefs)
    : DomDistillerRequestViewBase(distilled_page_prefs),
      expected_url_(expected_url),
      waiting_for_page_ready_(true) {
  content::WebContentsObserver::Observe(web_contents);
  distilled_page_prefs_->AddObserver(this);
}

DomDistillerViewerSource::RequestViewerHandle::~RequestViewerHandle() {
  distilled_page_prefs_->RemoveObserver(this);
}

void DomDistillerViewerSource::RequestViewerHandle::SendJavaScript(
    const std::string& buffer) {
  if (waiting_for_page_ready_) {
    buffers_.push_back(buffer);
  } else {
    DCHECK(buffers_.empty());
    if (web_contents()) {
      RunIsolatedJavaScript(web_contents()->GetPrimaryMainFrame(), buffer);
    }
  }
}

void DomDistillerViewerSource::RequestViewerHandle::DidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  if (!navigation_handle->IsInPrimaryMainFrame() ||
      !navigation_handle->HasCommitted())
    return;

  const GURL& navigation = navigation_handle->GetURL();
  bool expected_main_view_request = navigation == expected_url_;
  if (navigation_handle->IsSameDocument() || expected_main_view_request) {
    // In-page navigations, as well as the main view request can be ignored.
    return;
  }

  // At the moment we destroy the handle and won't be able
  // to restore the document later, so we prevent the page
  // from being stored in back-forward cache.
  content::BackForwardCache::DisableForRenderFrameHost(
      navigation_handle->GetPreviousRenderFrameHostId(),
      back_forward_cache::DisabledReason(
          back_forward_cache::DisabledReasonId::kDomDistillerViewerSource));

  Cancel();
}

void DomDistillerViewerSource::RequestViewerHandle::
    PrimaryMainFrameRenderProcessGone(base::TerminationStatus status) {
  Cancel();
}

void DomDistillerViewerSource::RequestViewerHandle::WebContentsDestroyed() {
  Cancel();
}

void DomDistillerViewerSource::RequestViewerHandle::Cancel() {
  // No need to listen for notifications.
  content::WebContentsObserver::Observe(nullptr);

  // Schedule the Viewer for deletion. Ensures distillation is cancelled, and
  // any pending data stored in |buffer_| is released.
  base::SingleThreadTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE,
                                                                this);
}

void DomDistillerViewerSource::RequestViewerHandle::DOMContentLoaded(
    content::RenderFrameHost* render_frame_host) {
  // DOMContentLoaded() is late enough to execute JavaScript, and is early
  // enough so that it's more likely that the title and content can be picked up
  // by TalkBack instead of the placeholder. If distillation is finished by
  // DOMContentLoaded(), onload() event would also be delayed, so that the
  // accessibility focus is more likely to be on the web content. Otherwise, the
  // focus is usually on the close button of the CustomTab (CCT), or nowhere. If
  // distillation finishes later than DOMContentLoaded(), or if for some
  // reason the accessibility focus is on the close button of the CCT, the title
  // could go unannounced.
  // See http://crbug.com/811417.
  if (render_frame_host->GetParentOrOuterDocument()) {
    return;
  }

  // Execute the scripts in buffer_ one-by-one, starting from the front of the
  // list.
  while (!buffers_.empty()) {
    RunIsolatedJavaScript(web_contents()->GetPrimaryMainFrame(),
                          buffers_.front());
    buffers_.pop_front();
  }
  // No SendJavaScript() calls allowed before |buffer_| is run and cleared.
  waiting_for_page_ready_ = false;
  // No need to Cancel() here.
}

DomDistillerViewerSource::DomDistillerViewerSource(
    DomDistillerServiceInterface* dom_distiller_service)
    : scheme_(kDomDistillerScheme),
      dom_distiller_service_(dom_distiller_service) {}

DomDistillerViewerSource::~DomDistillerViewerSource() = default;

std::string DomDistillerViewerSource::GetSource() {
  return scheme_ + "://";
}

void DomDistillerViewerSource::StartDataRequest(
    const GURL& url,
    const content::WebContents::Getter& wc_getter,
    content::URLDataSource::GotDataCallback callback) {
  // TODO(crbug.com/40050262): simplify path matching.
  const std::string path = URLDataSource::URLToRequestPath(url);
  content::WebContents* web_contents = wc_getter.Run();
  if (!web_contents)
    return;
#if !BUILDFLAG(IS_ANDROID)
  // Don't allow loading of mixed content on Reader Mode pages.
  blink::web_pref::WebPreferences prefs =
      web_contents->GetOrCreateWebPreferences();
  prefs.strict_mixed_content_checking = true;
  web_contents->SetWebPreferences(prefs);
#endif  // !BUILDFLAG(IS_ANDROID)
  if (kViewerCssPath == path) {
    std::string css = viewer::GetCss();
    std::move(callback).Run(
        base::MakeRefCounted<base::RefCountedString>(std::move(css)));
    return;
  }
  if (kViewerLoadingImagePath == path) {
    std::string image = viewer::GetLoadingImage();
    std::move(callback).Run(
        base::MakeRefCounted<base::RefCountedString>(std::move(image)));
    return;
  }
  auto remainder = base::RemovePrefix(path, kViewerSaveFontScalingPath);
  if (remainder) {
    double scale = 1.0;
    if (base::StringToDouble(*remainder, &scale)) {
      dom_distiller_service_->GetDistilledPagePrefs()->SetFontScaling(scale);
    }
  }

  // We need the host part to validate the parameter, but it's not available
  // from |URLDataSource|. |web_contents| is the most convenient place to
  // obtain the full URL.
  // TODO(crbug.com/40095934): pass GURL in URLDataSource::StartDataRequest().
  const std::string query = GURL("https://host/" + path).query();
  GURL request_url = web_contents->GetVisibleURL();
  // The query should match what's seen in |web_contents|.
  // For javascript:window.open(), it's not the case, but it's not a supported
  // use case.
  if (request_url.query() != query || request_url.path() != "/") {
    request_url = GURL();
  }
  RequestViewerHandle* request_viewer_handle =
      new RequestViewerHandle(web_contents, request_url,
                              dom_distiller_service_->GetDistilledPagePrefs());
  std::unique_ptr<ViewerHandle> viewer_handle = viewer::CreateViewRequest(
      dom_distiller_service_, request_url, request_viewer_handle,
      web_contents->GetContainerBounds().size());

  GURL current_url(url_utils::GetOriginalUrlFromDistillerUrl(request_url));

  // Pass an empty nonce value as the CSP is only inlined on the iOS build.
  std::string unsafe_page_html = viewer::GetArticleTemplateHtml(
      dom_distiller_service_->GetDistilledPagePrefs()->GetTheme(),
      dom_distiller_service_->GetDistilledPagePrefs()->GetFontFamily(),
      std::string(), /*use_offline_data=*/false);

  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(std::move(viewer_handle));
  } else {
    request_viewer_handle->FlagAsErrorPage();
  }

  // Place template on the page.
  std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>(
      std::move(unsafe_page_html)));
}

std::string DomDistillerViewerSource::GetMimeType(const GURL& url) {
  const std::string_view path = url.path_piece().substr(1);
  if (kViewerCssPath == path)
    return "text/css";
  if (kViewerLoadingImagePath == path)
    return "image/svg+xml";
  return "text/html";
}

bool DomDistillerViewerSource::ShouldServiceRequest(
    const GURL& url,
    content::BrowserContext* browser_context,
    int render_process_id) {
  return url.SchemeIs(scheme_);
}

std::string DomDistillerViewerSource::GetContentSecurityPolicy(
    network::mojom::CSPDirectiveName directive) {
  if (directive == network::mojom::CSPDirectiveName::StyleSrc) {
    return "style-src 'self' https://fonts.googleapis.com;";
  } else if (directive == network::mojom::CSPDirectiveName::ChildSrc) {
    return "child-src *;";
  } else if (directive ==
                 network::mojom::CSPDirectiveName::RequireTrustedTypesFor ||
             directive == network::mojom::CSPDirectiveName::TrustedTypes) {
    // This removes require-trusted-types-for and trusted-types directives
    // from the CSP header.
    return std::string();
  }

  return content::URLDataSource::GetContentSecurityPolicy(directive);
}

}  // namespace dom_distiller