1// Copyright (c) 2012 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 "content/browser/site_per_process_browsertest.h"
6
7#include "base/command_line.h"
8#include "base/strings/stringprintf.h"
9#include "content/browser/frame_host/cross_process_frame_connector.h"
10#include "content/browser/frame_host/frame_tree.h"
11#include "content/browser/frame_host/render_frame_proxy_host.h"
12#include "content/browser/frame_host/render_widget_host_view_child_frame.h"
13#include "content/browser/renderer_host/render_view_host_impl.h"
14#include "content/browser/web_contents/web_contents_impl.h"
15#include "content/public/browser/notification_observer.h"
16#include "content/public/browser/notification_service.h"
17#include "content/public/browser/notification_types.h"
18#include "content/public/browser/web_contents_observer.h"
19#include "content/public/common/content_switches.h"
20#include "content/public/test/browser_test_utils.h"
21#include "content/public/test/content_browser_test_utils.h"
22#include "content/public/test/test_utils.h"
23#include "content/shell/browser/shell.h"
24#include "content/test/content_browser_test_utils_internal.h"
25#include "net/dns/mock_host_resolver.h"
26
27namespace content {
28
29class SitePerProcessWebContentsObserver: public WebContentsObserver {
30 public:
31  explicit SitePerProcessWebContentsObserver(WebContents* web_contents)
32      : WebContentsObserver(web_contents),
33        navigation_succeeded_(false) {}
34  virtual ~SitePerProcessWebContentsObserver() {}
35
36  virtual void DidStartProvisionalLoadForFrame(
37      RenderFrameHost* render_frame_host,
38      const GURL& validated_url,
39      bool is_error_page,
40      bool is_iframe_srcdoc) OVERRIDE {
41    navigation_succeeded_ = false;
42  }
43
44  virtual void DidFailProvisionalLoad(
45      RenderFrameHost* render_frame_host,
46      const GURL& validated_url,
47      int error_code,
48      const base::string16& error_description) OVERRIDE {
49    navigation_url_ = validated_url;
50    navigation_succeeded_ = false;
51  }
52
53  virtual void DidCommitProvisionalLoadForFrame(
54      RenderFrameHost* render_frame_host,
55      const GURL& url,
56      ui::PageTransition transition_type) OVERRIDE {
57    navigation_url_ = url;
58    navigation_succeeded_ = true;
59  }
60
61  const GURL& navigation_url() const {
62    return navigation_url_;
63  }
64
65  int navigation_succeeded() const { return navigation_succeeded_; }
66
67 private:
68  GURL navigation_url_;
69  bool navigation_succeeded_;
70
71  DISALLOW_COPY_AND_ASSIGN(SitePerProcessWebContentsObserver);
72};
73
74class RedirectNotificationObserver : public NotificationObserver {
75 public:
76  // Register to listen for notifications of the given type from either a
77  // specific source, or from all sources if |source| is
78  // NotificationService::AllSources().
79  RedirectNotificationObserver(int notification_type,
80                               const NotificationSource& source);
81  virtual ~RedirectNotificationObserver();
82
83  // Wait until the specified notification occurs.  If the notification was
84  // emitted between the construction of this object and this call then it
85  // returns immediately.
86  void Wait();
87
88  // Returns NotificationService::AllSources() if we haven't observed a
89  // notification yet.
90  const NotificationSource& source() const {
91    return source_;
92  }
93
94  const NotificationDetails& details() const {
95    return details_;
96  }
97
98  // NotificationObserver:
99  virtual void Observe(int type,
100                       const NotificationSource& source,
101                       const NotificationDetails& details) OVERRIDE;
102
103 private:
104  bool seen_;
105  bool seen_twice_;
106  bool running_;
107  NotificationRegistrar registrar_;
108
109  NotificationSource source_;
110  NotificationDetails details_;
111  scoped_refptr<MessageLoopRunner> message_loop_runner_;
112
113  DISALLOW_COPY_AND_ASSIGN(RedirectNotificationObserver);
114};
115
116RedirectNotificationObserver::RedirectNotificationObserver(
117    int notification_type,
118    const NotificationSource& source)
119    : seen_(false),
120      running_(false),
121      source_(NotificationService::AllSources()) {
122  registrar_.Add(this, notification_type, source);
123}
124
125RedirectNotificationObserver::~RedirectNotificationObserver() {}
126
127void RedirectNotificationObserver::Wait() {
128  if (seen_ && seen_twice_)
129    return;
130
131  running_ = true;
132  message_loop_runner_ = new MessageLoopRunner;
133  message_loop_runner_->Run();
134  EXPECT_TRUE(seen_);
135}
136
137void RedirectNotificationObserver::Observe(
138    int type,
139    const NotificationSource& source,
140    const NotificationDetails& details) {
141  source_ = source;
142  details_ = details;
143  seen_twice_ = seen_;
144  seen_ = true;
145  if (!running_)
146    return;
147
148  message_loop_runner_->Quit();
149  running_ = false;
150}
151
152//
153// SitePerProcessBrowserTest
154//
155
156SitePerProcessBrowserTest::SitePerProcessBrowserTest() {
157};
158
159void SitePerProcessBrowserTest::StartFrameAtDataURL() {
160  std::string data_url_script =
161      "var iframes = document.getElementById('test');iframes.src="
162      "'data:text/html,dataurl';";
163  ASSERT_TRUE(ExecuteScript(shell()->web_contents(), data_url_script));
164}
165
166bool SitePerProcessBrowserTest::NavigateIframeToURL(Shell* window,
167                                                    const GURL& url,
168                                                    std::string iframe_id) {
169  // TODO(creis): This should wait for LOAD_STOP, but cross-site subframe
170  // navigations generate extra DidStartLoading and DidStopLoading messages.
171  // Until we replace swappedout:// with frame proxies, we need to listen for
172  // something else.  For now, we trigger NEW_SUBFRAME navigations and listen
173  // for commit.
174  std::string script = base::StringPrintf(
175      "setTimeout(\""
176      "var iframes = document.getElementById('%s');iframes.src='%s';"
177      "\",0)",
178      iframe_id.c_str(), url.spec().c_str());
179  WindowedNotificationObserver load_observer(
180      NOTIFICATION_NAV_ENTRY_COMMITTED,
181      Source<NavigationController>(
182          &window->web_contents()->GetController()));
183  bool result = ExecuteScript(window->web_contents(), script);
184  load_observer.Wait();
185  return result;
186}
187
188void SitePerProcessBrowserTest::SetUpCommandLine(CommandLine* command_line) {
189  command_line->AppendSwitch(switches::kSitePerProcess);
190};
191
192// It fails on ChromeOS and Android, so disabled while investigating.
193// http://crbug.com/399775
194#if defined(OS_ANDROID) || defined(OS_CHROMEOS)
195#define MAYBE_CrossSiteIframe DISABLED_CrossSiteIframe
196#else
197#define MAYBE_CrossSiteIframe CrossSiteIframe
198#endif
199IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, MAYBE_CrossSiteIframe) {
200  host_resolver()->AddRule("*", "127.0.0.1");
201  ASSERT_TRUE(test_server()->Start());
202  GURL main_url(test_server()->GetURL("files/site_per_process_main.html"));
203  NavigateToURL(shell(), main_url);
204
205  // It is safe to obtain the root frame tree node here, as it doesn't change.
206  FrameTreeNode* root =
207      static_cast<WebContentsImpl*>(shell()->web_contents())->
208          GetFrameTree()->root();
209
210  SitePerProcessWebContentsObserver observer(shell()->web_contents());
211
212  // Load same-site page into iframe.
213  FrameTreeNode* child = root->child_at(0);
214  GURL http_url(test_server()->GetURL("files/title1.html"));
215  NavigateFrameToURL(child, http_url);
216  EXPECT_EQ(http_url, observer.navigation_url());
217  EXPECT_TRUE(observer.navigation_succeeded());
218  {
219    // There should be only one RenderWidgetHost when there are no
220    // cross-process iframes.
221    std::set<RenderWidgetHostView*> views_set =
222        static_cast<WebContentsImpl*>(shell()->web_contents())
223            ->GetRenderWidgetHostViewsInTree();
224    EXPECT_EQ(1U, views_set.size());
225  }
226  RenderFrameProxyHost* proxy_to_parent =
227      child->render_manager()->GetRenderFrameProxyHost(
228          shell()->web_contents()->GetSiteInstance());
229  EXPECT_FALSE(proxy_to_parent);
230
231  // These must stay in scope with replace_host.
232  GURL::Replacements replace_host;
233  std::string foo_com("foo.com");
234
235  // Load cross-site page into iframe.
236  GURL cross_site_url(test_server()->GetURL("files/title2.html"));
237  replace_host.SetHostStr(foo_com);
238  cross_site_url = cross_site_url.ReplaceComponents(replace_host);
239  NavigateFrameToURL(root->child_at(0), cross_site_url);
240  EXPECT_EQ(cross_site_url, observer.navigation_url());
241  EXPECT_TRUE(observer.navigation_succeeded());
242
243  // Ensure that we have created a new process for the subframe.
244  ASSERT_EQ(1U, root->child_count());
245  SiteInstance* site_instance = child->current_frame_host()->GetSiteInstance();
246  RenderViewHost* rvh = child->current_frame_host()->render_view_host();
247  RenderProcessHost* rph = child->current_frame_host()->GetProcess();
248  EXPECT_NE(shell()->web_contents()->GetRenderViewHost(), rvh);
249  EXPECT_NE(shell()->web_contents()->GetSiteInstance(), site_instance);
250  EXPECT_NE(shell()->web_contents()->GetRenderProcessHost(), rph);
251  {
252    // There should be now two RenderWidgetHosts, one for each process
253    // rendering a frame.
254    std::set<RenderWidgetHostView*> views_set =
255        static_cast<WebContentsImpl*>(shell()->web_contents())
256            ->GetRenderWidgetHostViewsInTree();
257    EXPECT_EQ(2U, views_set.size());
258  }
259  proxy_to_parent = child->render_manager()->GetProxyToParent();
260  EXPECT_TRUE(proxy_to_parent);
261  EXPECT_TRUE(proxy_to_parent->cross_process_frame_connector());
262  EXPECT_EQ(
263      rvh->GetView(),
264      proxy_to_parent->cross_process_frame_connector()->get_view_for_testing());
265
266  // Load another cross-site page into the same iframe.
267  cross_site_url = test_server()->GetURL("files/title3.html");
268  std::string bar_com("bar.com");
269  replace_host.SetHostStr(bar_com);
270  cross_site_url = cross_site_url.ReplaceComponents(replace_host);
271  NavigateFrameToURL(root->child_at(0), cross_site_url);
272  EXPECT_EQ(cross_site_url, observer.navigation_url());
273  EXPECT_TRUE(observer.navigation_succeeded());
274
275  // Check again that a new process is created and is different from the
276  // top level one and the previous one.
277  ASSERT_EQ(1U, root->child_count());
278  child = root->child_at(0);
279  EXPECT_NE(shell()->web_contents()->GetRenderViewHost(),
280            child->current_frame_host()->render_view_host());
281  EXPECT_NE(rvh, child->current_frame_host()->render_view_host());
282  EXPECT_NE(shell()->web_contents()->GetSiteInstance(),
283            child->current_frame_host()->GetSiteInstance());
284  EXPECT_NE(site_instance,
285            child->current_frame_host()->GetSiteInstance());
286  EXPECT_NE(shell()->web_contents()->GetRenderProcessHost(),
287            child->current_frame_host()->GetProcess());
288  EXPECT_NE(rph, child->current_frame_host()->GetProcess());
289  {
290    std::set<RenderWidgetHostView*> views_set =
291        static_cast<WebContentsImpl*>(shell()->web_contents())
292            ->GetRenderWidgetHostViewsInTree();
293    EXPECT_EQ(2U, views_set.size());
294  }
295  EXPECT_EQ(proxy_to_parent, child->render_manager()->GetProxyToParent());
296  EXPECT_TRUE(proxy_to_parent->cross_process_frame_connector());
297  EXPECT_EQ(
298      child->current_frame_host()->render_view_host()->GetView(),
299      proxy_to_parent->cross_process_frame_connector()->get_view_for_testing());
300}
301
302// Crash a subframe and ensures its children are cleared from the FrameTree.
303// See http://crbug.com/338508.
304// TODO(creis): Disabled for flakiness; see http://crbug.com/405582.
305// TODO(creis): Enable this on Android when we can kill the process there.
306IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest, DISABLED_CrashSubframe) {
307  host_resolver()->AddRule("*", "127.0.0.1");
308  ASSERT_TRUE(test_server()->Start());
309  GURL main_url(test_server()->GetURL("files/site_per_process_main.html"));
310  NavigateToURL(shell(), main_url);
311
312  StartFrameAtDataURL();
313
314  // These must stay in scope with replace_host.
315  GURL::Replacements replace_host;
316  std::string foo_com("foo.com");
317
318  // Load cross-site page into iframe.
319  GURL cross_site_url(test_server()->GetURL("files/title2.html"));
320  replace_host.SetHostStr(foo_com);
321  cross_site_url = cross_site_url.ReplaceComponents(replace_host);
322  EXPECT_TRUE(NavigateIframeToURL(shell(), cross_site_url, "test"));
323
324  // Check the subframe process.
325  FrameTreeNode* root =
326      static_cast<WebContentsImpl*>(shell()->web_contents())->
327          GetFrameTree()->root();
328  ASSERT_EQ(1U, root->child_count());
329  FrameTreeNode* child = root->child_at(0);
330  EXPECT_EQ(main_url, root->current_url());
331  EXPECT_EQ(cross_site_url, child->current_url());
332
333  EXPECT_TRUE(
334      child->current_frame_host()->render_view_host()->IsRenderViewLive());
335  EXPECT_TRUE(child->current_frame_host()->IsRenderFrameLive());
336
337  // Crash the subframe process.
338  RenderProcessHost* root_process = root->current_frame_host()->GetProcess();
339  RenderProcessHost* child_process = child->current_frame_host()->GetProcess();
340  {
341    RenderProcessHostWatcher crash_observer(
342        child_process,
343        RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
344    base::KillProcess(child_process->GetHandle(), 0, false);
345    crash_observer.Wait();
346  }
347
348  // Ensure that the child frame still exists but has been cleared.
349  EXPECT_EQ(1U, root->child_count());
350  EXPECT_EQ(main_url, root->current_url());
351  EXPECT_EQ(GURL(), child->current_url());
352
353  EXPECT_FALSE(
354      child->current_frame_host()->render_view_host()->IsRenderViewLive());
355  EXPECT_FALSE(child->current_frame_host()->IsRenderFrameLive());
356  EXPECT_FALSE(child->current_frame_host()->render_frame_created_);
357
358  // Now crash the top-level page to clear the child frame.
359  {
360    RenderProcessHostWatcher crash_observer(
361        root_process,
362        RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
363    base::KillProcess(root_process->GetHandle(), 0, false);
364    crash_observer.Wait();
365  }
366  EXPECT_EQ(0U, root->child_count());
367  EXPECT_EQ(GURL(), root->current_url());
368}
369
370// TODO(nasko): Disable this test until out-of-process iframes is ready and the
371// security checks are back in place.
372// TODO(creis): Replace SpawnedTestServer with host_resolver to get test to run
373// on Android (http://crbug.com/187570).
374IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
375                       DISABLED_CrossSiteIframeRedirectOnce) {
376  ASSERT_TRUE(test_server()->Start());
377  net::SpawnedTestServer https_server(
378      net::SpawnedTestServer::TYPE_HTTPS,
379      net::SpawnedTestServer::kLocalhost,
380      base::FilePath(FILE_PATH_LITERAL("content/test/data")));
381  ASSERT_TRUE(https_server.Start());
382
383  GURL main_url(test_server()->GetURL("files/site_per_process_main.html"));
384  GURL http_url(test_server()->GetURL("files/title1.html"));
385  GURL https_url(https_server.GetURL("files/title1.html"));
386
387  NavigateToURL(shell(), main_url);
388
389  SitePerProcessWebContentsObserver observer(shell()->web_contents());
390  {
391    // Load cross-site client-redirect page into Iframe.
392    // Should be blocked.
393    GURL client_redirect_https_url(https_server.GetURL(
394        "client-redirect?files/title1.html"));
395    EXPECT_TRUE(NavigateIframeToURL(shell(),
396                                    client_redirect_https_url, "test"));
397    // DidFailProvisionalLoad when navigating to client_redirect_https_url.
398    EXPECT_EQ(observer.navigation_url(), client_redirect_https_url);
399    EXPECT_FALSE(observer.navigation_succeeded());
400  }
401
402  {
403    // Load cross-site server-redirect page into Iframe,
404    // which redirects to same-site page.
405    GURL server_redirect_http_url(https_server.GetURL(
406        "server-redirect?" + http_url.spec()));
407    EXPECT_TRUE(NavigateIframeToURL(shell(),
408                                    server_redirect_http_url, "test"));
409    EXPECT_EQ(observer.navigation_url(), http_url);
410    EXPECT_TRUE(observer.navigation_succeeded());
411  }
412
413  {
414    // Load cross-site server-redirect page into Iframe,
415    // which redirects to cross-site page.
416    GURL server_redirect_http_url(https_server.GetURL(
417        "server-redirect?files/title1.html"));
418    EXPECT_TRUE(NavigateIframeToURL(shell(),
419                                    server_redirect_http_url, "test"));
420    // DidFailProvisionalLoad when navigating to https_url.
421    EXPECT_EQ(observer.navigation_url(), https_url);
422    EXPECT_FALSE(observer.navigation_succeeded());
423  }
424
425  {
426    // Load same-site server-redirect page into Iframe,
427    // which redirects to cross-site page.
428    GURL server_redirect_http_url(test_server()->GetURL(
429        "server-redirect?" + https_url.spec()));
430    EXPECT_TRUE(NavigateIframeToURL(shell(),
431                                    server_redirect_http_url, "test"));
432
433    EXPECT_EQ(observer.navigation_url(), https_url);
434    EXPECT_FALSE(observer.navigation_succeeded());
435   }
436
437  {
438    // Load same-site client-redirect page into Iframe,
439    // which redirects to cross-site page.
440    GURL client_redirect_http_url(test_server()->GetURL(
441        "client-redirect?" + https_url.spec()));
442
443    RedirectNotificationObserver load_observer2(
444        NOTIFICATION_LOAD_STOP,
445        Source<NavigationController>(
446            &shell()->web_contents()->GetController()));
447
448    EXPECT_TRUE(NavigateIframeToURL(shell(),
449                                    client_redirect_http_url, "test"));
450
451    // Same-site Client-Redirect Page should be loaded successfully.
452    EXPECT_EQ(observer.navigation_url(), client_redirect_http_url);
453    EXPECT_TRUE(observer.navigation_succeeded());
454
455    // Redirecting to Cross-site Page should be blocked.
456    load_observer2.Wait();
457    EXPECT_EQ(observer.navigation_url(), https_url);
458    EXPECT_FALSE(observer.navigation_succeeded());
459  }
460
461  {
462    // Load same-site server-redirect page into Iframe,
463    // which redirects to same-site page.
464    GURL server_redirect_http_url(test_server()->GetURL(
465        "server-redirect?files/title1.html"));
466    EXPECT_TRUE(NavigateIframeToURL(shell(),
467                                    server_redirect_http_url, "test"));
468    EXPECT_EQ(observer.navigation_url(), http_url);
469    EXPECT_TRUE(observer.navigation_succeeded());
470   }
471
472  {
473    // Load same-site client-redirect page into Iframe,
474    // which redirects to same-site page.
475    GURL client_redirect_http_url(test_server()->GetURL(
476        "client-redirect?" + http_url.spec()));
477    RedirectNotificationObserver load_observer2(
478        NOTIFICATION_LOAD_STOP,
479        Source<NavigationController>(
480            &shell()->web_contents()->GetController()));
481
482    EXPECT_TRUE(NavigateIframeToURL(shell(),
483                                    client_redirect_http_url, "test"));
484
485    // Same-site Client-Redirect Page should be loaded successfully.
486    EXPECT_EQ(observer.navigation_url(), client_redirect_http_url);
487    EXPECT_TRUE(observer.navigation_succeeded());
488
489    // Redirecting to Same-site Page should be loaded successfully.
490    load_observer2.Wait();
491    EXPECT_EQ(observer.navigation_url(), http_url);
492    EXPECT_TRUE(observer.navigation_succeeded());
493  }
494}
495
496// TODO(nasko): Disable this test until out-of-process iframes is ready and the
497// security checks are back in place.
498// TODO(creis): Replace SpawnedTestServer with host_resolver to get test to run
499// on Android (http://crbug.com/187570).
500IN_PROC_BROWSER_TEST_F(SitePerProcessBrowserTest,
501                       DISABLED_CrossSiteIframeRedirectTwice) {
502  ASSERT_TRUE(test_server()->Start());
503  net::SpawnedTestServer https_server(
504      net::SpawnedTestServer::TYPE_HTTPS,
505      net::SpawnedTestServer::kLocalhost,
506      base::FilePath(FILE_PATH_LITERAL("content/test/data")));
507  ASSERT_TRUE(https_server.Start());
508
509  GURL main_url(test_server()->GetURL("files/site_per_process_main.html"));
510  GURL http_url(test_server()->GetURL("files/title1.html"));
511  GURL https_url(https_server.GetURL("files/title1.html"));
512
513  NavigateToURL(shell(), main_url);
514
515  SitePerProcessWebContentsObserver observer(shell()->web_contents());
516  {
517    // Load client-redirect page pointing to a cross-site client-redirect page,
518    // which eventually redirects back to same-site page.
519    GURL client_redirect_https_url(https_server.GetURL(
520        "client-redirect?" + http_url.spec()));
521    GURL client_redirect_http_url(test_server()->GetURL(
522        "client-redirect?" + client_redirect_https_url.spec()));
523
524    // We should wait until second client redirect get cancelled.
525    RedirectNotificationObserver load_observer2(
526        NOTIFICATION_LOAD_STOP,
527        Source<NavigationController>(
528            &shell()->web_contents()->GetController()));
529
530    EXPECT_TRUE(NavigateIframeToURL(shell(), client_redirect_http_url, "test"));
531
532    // DidFailProvisionalLoad when navigating to client_redirect_https_url.
533    load_observer2.Wait();
534    EXPECT_EQ(observer.navigation_url(), client_redirect_https_url);
535    EXPECT_FALSE(observer.navigation_succeeded());
536  }
537
538  {
539    // Load server-redirect page pointing to a cross-site server-redirect page,
540    // which eventually redirect back to same-site page.
541    GURL server_redirect_https_url(https_server.GetURL(
542        "server-redirect?" + http_url.spec()));
543    GURL server_redirect_http_url(test_server()->GetURL(
544        "server-redirect?" + server_redirect_https_url.spec()));
545    EXPECT_TRUE(NavigateIframeToURL(shell(),
546                                    server_redirect_http_url, "test"));
547    EXPECT_EQ(observer.navigation_url(), http_url);
548    EXPECT_TRUE(observer.navigation_succeeded());
549  }
550
551  {
552    // Load server-redirect page pointing to a cross-site server-redirect page,
553    // which eventually redirects back to cross-site page.
554    GURL server_redirect_https_url(https_server.GetURL(
555        "server-redirect?" + https_url.spec()));
556    GURL server_redirect_http_url(test_server()->GetURL(
557        "server-redirect?" + server_redirect_https_url.spec()));
558    EXPECT_TRUE(NavigateIframeToURL(shell(), server_redirect_http_url, "test"));
559
560    // DidFailProvisionalLoad when navigating to https_url.
561    EXPECT_EQ(observer.navigation_url(), https_url);
562    EXPECT_FALSE(observer.navigation_succeeded());
563  }
564
565  {
566    // Load server-redirect page pointing to a cross-site client-redirect page,
567    // which eventually redirects back to same-site page.
568    GURL client_redirect_http_url(https_server.GetURL(
569        "client-redirect?" + http_url.spec()));
570    GURL server_redirect_http_url(test_server()->GetURL(
571        "server-redirect?" + client_redirect_http_url.spec()));
572    EXPECT_TRUE(NavigateIframeToURL(shell(), server_redirect_http_url, "test"));
573
574    // DidFailProvisionalLoad when navigating to client_redirect_http_url.
575    EXPECT_EQ(observer.navigation_url(), client_redirect_http_url);
576    EXPECT_FALSE(observer.navigation_succeeded());
577  }
578}
579
580}  // namespace content
581