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. 4 5 #include <string.h> 6 7 #include "base/command_line.h" 8 #include "base/guid.h" 9 #include "base/path_service.h" 10 #include "base/strings/utf_string_conversions.h" 11 #include "chrome/browser/dom_distiller/dom_distiller_service_factory.h" 12 #include "chrome/browser/profiles/profile.h" 13 #include "chrome/browser/ui/browser.h" 14 #include "chrome/browser/ui/tabs/tab_strip_model.h" 15 #include "chrome/common/chrome_switches.h" 16 #include "chrome/test/base/in_process_browser_test.h" 17 #include "chrome/test/base/ui_test_utils.h" 18 #include "components/dom_distiller/content/dom_distiller_viewer_source.h" 19 #include "components/dom_distiller/core/article_entry.h" 20 #include "components/dom_distiller/core/distilled_page_prefs.h" 21 #include "components/dom_distiller/core/distiller.h" 22 #include "components/dom_distiller/core/dom_distiller_service.h" 23 #include "components/dom_distiller/core/dom_distiller_store.h" 24 #include "components/dom_distiller/core/dom_distiller_test_util.h" 25 #include "components/dom_distiller/core/fake_distiller.h" 26 #include "components/dom_distiller/core/fake_distiller_page.h" 27 #include "components/dom_distiller/core/task_tracker.h" 28 #include "components/dom_distiller/core/url_constants.h" 29 #include "components/dom_distiller/core/url_utils.h" 30 #include "components/leveldb_proto/testing/fake_db.h" 31 #include "content/public/browser/render_view_host.h" 32 #include "content/public/browser/url_data_source.h" 33 #include "content/public/browser/web_contents.h" 34 #include "content/public/browser/web_contents_observer.h" 35 #include "content/public/test/browser_test_utils.h" 36 #include "testing/gmock/include/gmock/gmock.h" 37 #include "testing/gtest/include/gtest/gtest.h" 38 39 namespace dom_distiller { 40 41 using leveldb_proto::test::FakeDB; 42 using test::FakeDistiller; 43 using test::MockDistillerPage; 44 using test::MockDistillerFactory; 45 using test::MockDistillerPageFactory; 46 using test::util::CreateStoreWithFakeDB; 47 using testing::HasSubstr; 48 using testing::Not; 49 50 namespace { 51 52 const char kGetLoadIndicatorClassName[] = 53 "window.domAutomationController.send(" 54 "document.getElementById('loadingIndicator').className)"; 55 56 const char kGetContent[] = 57 "window.domAutomationController.send(" 58 "document.getElementById('content').innerHTML)"; 59 60 const char kGetBodyClass[] = 61 "window.domAutomationController.send(" 62 "document.body.className)"; 63 64 void AddEntry(const ArticleEntry& e, FakeDB<ArticleEntry>::EntryMap* map) { 65 (*map)[e.entry_id()] = e; 66 } 67 68 ArticleEntry CreateEntry(std::string entry_id, std::string page_url) { 69 ArticleEntry entry; 70 entry.set_entry_id(entry_id); 71 if (!page_url.empty()) { 72 ArticleEntryPage* page = entry.add_pages(); 73 page->set_url(page_url); 74 } 75 return entry; 76 } 77 78 } // namespace 79 80 class DomDistillerViewerSourceBrowserTest : public InProcessBrowserTest { 81 public: 82 DomDistillerViewerSourceBrowserTest() {} 83 virtual ~DomDistillerViewerSourceBrowserTest() {} 84 85 virtual void SetUpOnMainThread() OVERRIDE { 86 database_model_ = new FakeDB<ArticleEntry>::EntryMap; 87 } 88 89 virtual void TearDownOnMainThread() OVERRIDE { delete database_model_; } 90 91 virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE { 92 command_line->AppendSwitch(switches::kEnableDomDistiller); 93 } 94 95 static KeyedService* Build(content::BrowserContext* context) { 96 FakeDB<ArticleEntry>* fake_db = new FakeDB<ArticleEntry>(database_model_); 97 distiller_factory_ = new MockDistillerFactory(); 98 MockDistillerPageFactory* distiller_page_factory_ = 99 new MockDistillerPageFactory(); 100 DomDistillerContextKeyedService* service = 101 new DomDistillerContextKeyedService( 102 scoped_ptr<DomDistillerStoreInterface>( 103 CreateStoreWithFakeDB(fake_db, 104 FakeDB<ArticleEntry>::EntryMap())), 105 scoped_ptr<DistillerFactory>(distiller_factory_), 106 scoped_ptr<DistillerPageFactory>(distiller_page_factory_), 107 scoped_ptr<DistilledPagePrefs>( 108 new DistilledPagePrefs( 109 Profile::FromBrowserContext( 110 context)->GetPrefs()))); 111 fake_db->InitCallback(true); 112 fake_db->LoadCallback(true); 113 if (expect_distillation_) { 114 // There will only be destillation of an article if the database contains 115 // the article. 116 FakeDistiller* distiller = new FakeDistiller(true); 117 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 118 .WillOnce(testing::Return(distiller)); 119 } 120 if (expect_distiller_page_) { 121 MockDistillerPage* distiller_page = new MockDistillerPage(); 122 EXPECT_CALL(*distiller_page_factory_, CreateDistillerPageImpl()) 123 .WillOnce(testing::Return(distiller_page)); 124 } 125 return service; 126 } 127 128 void ViewSingleDistilledPage(const GURL& url, 129 const std::string& expected_mime_type); 130 // Database entries. 131 static FakeDB<ArticleEntry>::EntryMap* database_model_; 132 static bool expect_distillation_; 133 static bool expect_distiller_page_; 134 static MockDistillerFactory* distiller_factory_; 135 }; 136 137 FakeDB<ArticleEntry>::EntryMap* 138 DomDistillerViewerSourceBrowserTest::database_model_; 139 bool DomDistillerViewerSourceBrowserTest::expect_distillation_ = false; 140 bool DomDistillerViewerSourceBrowserTest::expect_distiller_page_ = false; 141 MockDistillerFactory* DomDistillerViewerSourceBrowserTest::distiller_factory_ = 142 NULL; 143 144 // The DomDistillerViewerSource renders untrusted content, so ensure no bindings 145 // are enabled when the article exists in the database. 146 IN_PROC_BROWSER_TEST_F(DomDistillerViewerSourceBrowserTest, 147 NoWebUIBindingsArticleExists) { 148 // Ensure there is one item in the database, which will trigger distillation. 149 const ArticleEntry entry = CreateEntry("DISTILLED", "http://example.com/1"); 150 AddEntry(entry, database_model_); 151 expect_distillation_ = true; 152 expect_distiller_page_ = true; 153 const GURL url = url_utils::GetDistillerViewUrlFromEntryId( 154 kDomDistillerScheme, entry.entry_id()); 155 ViewSingleDistilledPage(url, "text/html"); 156 } 157 158 // The DomDistillerViewerSource renders untrusted content, so ensure no bindings 159 // are enabled when the article is not found. 160 IN_PROC_BROWSER_TEST_F(DomDistillerViewerSourceBrowserTest, 161 NoWebUIBindingsArticleNotFound) { 162 // The article does not exist, so assume no distillation will happen. 163 expect_distillation_ = false; 164 expect_distiller_page_ = false; 165 const GURL url = url_utils::GetDistillerViewUrlFromEntryId( 166 kDomDistillerScheme, "DOES_NOT_EXIST"); 167 ViewSingleDistilledPage(url, "text/html"); 168 } 169 170 // The DomDistillerViewerSource renders untrusted content, so ensure no bindings 171 // are enabled when requesting to view an arbitrary URL. 172 IN_PROC_BROWSER_TEST_F(DomDistillerViewerSourceBrowserTest, 173 NoWebUIBindingsViewUrl) { 174 // We should expect distillation for any valid URL. 175 expect_distillation_ = true; 176 expect_distiller_page_ = true; 177 GURL view_url("http://www.example.com/1"); 178 const GURL url = 179 url_utils::GetDistillerViewUrlFromUrl(kDomDistillerScheme, view_url); 180 ViewSingleDistilledPage(url, "text/html"); 181 } 182 183 void DomDistillerViewerSourceBrowserTest::ViewSingleDistilledPage( 184 const GURL& url, 185 const std::string& expected_mime_type) { 186 // Ensure the correct factory is used for the DomDistillerService. 187 dom_distiller::DomDistillerServiceFactory::GetInstance() 188 ->SetTestingFactoryAndUse(browser()->profile(), &Build); 189 190 // Navigate to a URL which the source should respond to. 191 ui_test_utils::NavigateToURL(browser(), url); 192 193 // Ensure no bindings for the loaded |url|. 194 content::WebContents* contents_after_nav = 195 browser()->tab_strip_model()->GetActiveWebContents(); 196 ASSERT_TRUE(contents_after_nav != NULL); 197 EXPECT_EQ(url, contents_after_nav->GetLastCommittedURL()); 198 const content::RenderViewHost* render_view_host = 199 contents_after_nav->GetRenderViewHost(); 200 EXPECT_EQ(0, render_view_host->GetEnabledBindings()); 201 EXPECT_EQ(expected_mime_type, contents_after_nav->GetContentsMimeType()); 202 } 203 204 // The DomDistillerViewerSource renders untrusted content, so ensure no bindings 205 // are enabled when the CSS resource is loaded. This CSS might be bundle with 206 // Chrome or provided by an extension. 207 IN_PROC_BROWSER_TEST_F(DomDistillerViewerSourceBrowserTest, 208 NoWebUIBindingsDisplayCSS) { 209 expect_distillation_ = false; 210 expect_distiller_page_ = false; 211 // Navigate to a URL which the source should respond to with CSS. 212 std::string url_without_scheme = std::string("://foobar/") + kViewerCssPath; 213 GURL url(kDomDistillerScheme + url_without_scheme); 214 ViewSingleDistilledPage(url, "text/css"); 215 } 216 217 IN_PROC_BROWSER_TEST_F(DomDistillerViewerSourceBrowserTest, 218 EmptyURLShouldNotCrash) { 219 // This is a bogus URL, so no distillation will happen. 220 expect_distillation_ = false; 221 expect_distiller_page_ = false; 222 const GURL url(std::string(kDomDistillerScheme) + "://bogus/"); 223 ViewSingleDistilledPage(url, "text/html"); 224 } 225 226 IN_PROC_BROWSER_TEST_F(DomDistillerViewerSourceBrowserTest, 227 InvalidURLShouldNotCrash) { 228 // This is a bogus URL, so no distillation will happen. 229 expect_distillation_ = false; 230 expect_distiller_page_ = false; 231 const GURL url(std::string(kDomDistillerScheme) + "://bogus/foobar"); 232 ViewSingleDistilledPage(url, "text/html"); 233 } 234 235 IN_PROC_BROWSER_TEST_F(DomDistillerViewerSourceBrowserTest, 236 MultiPageArticle) { 237 expect_distillation_ = false; 238 expect_distiller_page_ = true; 239 dom_distiller::DomDistillerServiceFactory::GetInstance() 240 ->SetTestingFactoryAndUse(browser()->profile(), &Build); 241 242 scoped_refptr<content::MessageLoopRunner> distillation_done_runner = 243 new content::MessageLoopRunner; 244 245 FakeDistiller* distiller = new FakeDistiller( 246 false, 247 distillation_done_runner->QuitClosure()); 248 EXPECT_CALL(*distiller_factory_, CreateDistillerImpl()) 249 .WillOnce(testing::Return(distiller)); 250 251 // Setup observer to inspect the RenderViewHost after committed navigation. 252 content::WebContents* contents = 253 browser()->tab_strip_model()->GetActiveWebContents(); 254 255 // Navigate to a URL and wait for the distiller to flush contents to the page. 256 GURL url(dom_distiller::url_utils::GetDistillerViewUrlFromUrl( 257 kDomDistillerScheme, GURL("http://urlthatlooksvalid.com"))); 258 chrome::NavigateParams params(browser(), url, ui::PAGE_TRANSITION_TYPED); 259 chrome::Navigate(¶ms); 260 distillation_done_runner->Run(); 261 262 // Fake a multi-page response from distiller. 263 264 std::vector<scoped_refptr<ArticleDistillationUpdate::RefCountedPageProto> > 265 update_pages; 266 scoped_ptr<DistilledArticleProto> article(new DistilledArticleProto()); 267 268 // Flush page 1. 269 { 270 scoped_refptr<base::RefCountedData<DistilledPageProto> > page_proto = 271 new base::RefCountedData<DistilledPageProto>(); 272 page_proto->data.set_url("http://foobar.1.html"); 273 page_proto->data.set_html("<div>Page 1 content</div>"); 274 update_pages.push_back(page_proto); 275 *(article->add_pages()) = page_proto->data; 276 277 ArticleDistillationUpdate update(update_pages, true, false); 278 distiller->RunDistillerUpdateCallback(update); 279 280 // Wait for the page load to complete as the first page completes the root 281 // document. 282 content::WaitForLoadStop(contents); 283 284 std::string result; 285 EXPECT_TRUE(content::ExecuteScriptAndExtractString( 286 contents, kGetLoadIndicatorClassName , &result)); 287 EXPECT_EQ("visible", result); 288 289 EXPECT_TRUE(content::ExecuteScriptAndExtractString( 290 contents, kGetContent , &result)); 291 EXPECT_THAT(result, HasSubstr("Page 1 content")); 292 EXPECT_THAT(result, Not(HasSubstr("Page 2 content"))); 293 } 294 295 // Flush page 2. 296 { 297 scoped_refptr<base::RefCountedData<DistilledPageProto> > page_proto = 298 new base::RefCountedData<DistilledPageProto>(); 299 page_proto->data.set_url("http://foobar.2.html"); 300 page_proto->data.set_html("<div>Page 2 content</div>"); 301 update_pages.push_back(page_proto); 302 *(article->add_pages()) = page_proto->data; 303 304 ArticleDistillationUpdate update(update_pages, false, false); 305 distiller->RunDistillerUpdateCallback(update); 306 307 std::string result; 308 EXPECT_TRUE(content::ExecuteScriptAndExtractString( 309 contents, kGetLoadIndicatorClassName , &result)); 310 EXPECT_EQ("visible", result); 311 312 EXPECT_TRUE(content::ExecuteScriptAndExtractString( 313 contents, kGetContent , &result)); 314 EXPECT_THAT(result, HasSubstr("Page 1 content")); 315 EXPECT_THAT(result, HasSubstr("Page 2 content")); 316 } 317 318 // Complete the load. 319 distiller->RunDistillerCallback(article.Pass()); 320 base::RunLoop().RunUntilIdle(); 321 322 std::string result; 323 EXPECT_TRUE(content::ExecuteScriptAndExtractString( 324 contents, kGetLoadIndicatorClassName, &result)); 325 EXPECT_EQ("hidden", result); 326 EXPECT_TRUE(content::ExecuteScriptAndExtractString( 327 contents, kGetContent , &result)); 328 EXPECT_THAT(result, HasSubstr("Page 1 content")); 329 EXPECT_THAT(result, HasSubstr("Page 2 content")); 330 } 331 332 IN_PROC_BROWSER_TEST_F(DomDistillerViewerSourceBrowserTest, PrefChange) { 333 expect_distillation_ = true; 334 expect_distiller_page_ = true; 335 GURL view_url("http://www.example.com/1"); 336 content::WebContents* contents = 337 browser()->tab_strip_model()->GetActiveWebContents(); 338 const GURL url = 339 url_utils::GetDistillerViewUrlFromUrl(kDomDistillerScheme, view_url); 340 ViewSingleDistilledPage(url, "text/html"); 341 content::WaitForLoadStop(contents); 342 std::string result; 343 EXPECT_TRUE(content::ExecuteScriptAndExtractString( 344 contents, kGetBodyClass, &result)); 345 EXPECT_EQ("light sans-serif", result); 346 347 DistilledPagePrefs* distilled_page_prefs = 348 DomDistillerServiceFactory::GetForBrowserContext( 349 browser()->profile())->GetDistilledPagePrefs(); 350 351 distilled_page_prefs->SetTheme(DistilledPagePrefs::DARK); 352 base::RunLoop().RunUntilIdle(); 353 EXPECT_TRUE(content::ExecuteScriptAndExtractString( 354 contents, kGetBodyClass, &result)); 355 EXPECT_EQ("dark sans-serif", result); 356 357 distilled_page_prefs->SetFontFamily(DistilledPagePrefs::SERIF); 358 base::RunLoop().RunUntilIdle(); 359 EXPECT_TRUE( 360 content::ExecuteScriptAndExtractString(contents, kGetBodyClass, &result)); 361 EXPECT_EQ("dark serif", result); 362 } 363 364 } // namespace dom_distiller 365