1 // Copyright 2017 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 "chrome/browser/ui/omnibox/chrome_omnibox_navigation_observer.h"
6 
7 #include <unordered_map>
8 #include <vector>
9 
10 #include "base/run_loop.h"
11 #include "base/stl_util.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/infobars/infobar_service.h"
15 #include "chrome/browser/search_engines/template_url_service_factory.h"
16 #include "chrome/browser/search_engines/template_url_service_factory_test_util.h"
17 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
18 #include "chrome/test/base/testing_profile.h"
19 #include "components/search_engines/template_url.h"
20 #include "components/search_engines/template_url_data.h"
21 #include "components/search_engines/template_url_service.h"
22 #include "content/public/browser/navigation_details.h"
23 #include "content/public/browser/navigation_entry.h"
24 #include "content/public/browser/notification_details.h"
25 #include "content/public/browser/notification_service.h"
26 #include "content/public/browser/notification_source.h"
27 #include "content/public/browser/notification_types.h"
28 #include "content/public/browser/web_contents.h"
29 #include "net/http/http_response_headers.h"
30 #include "net/http/http_status_code.h"
31 #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
32 #include "services/network/test/test_url_loader_factory.h"
33 #include "services/network/test/test_utils.h"
34 
35 using network::TestURLLoaderFactory;
36 
37 // A trival ChromeOmniboxNavigationObserver that keeps track of whether
38 // CreateAlternateNavInfoBar() has been called.
39 class MockChromeOmniboxNavigationObserver
40     : public ChromeOmniboxNavigationObserver {
41  public:
MockChromeOmniboxNavigationObserver(Profile * profile,const base::string16 & text,const AutocompleteMatch & match,const AutocompleteMatch & alternate_nav_match,bool * displayed_infobar)42   MockChromeOmniboxNavigationObserver(
43       Profile* profile,
44       const base::string16& text,
45       const AutocompleteMatch& match,
46       const AutocompleteMatch& alternate_nav_match,
47       bool* displayed_infobar)
48       : ChromeOmniboxNavigationObserver(profile,
49                                         text,
50                                         match,
51                                         alternate_nav_match),
52         displayed_infobar_(displayed_infobar) {
53     *displayed_infobar_ = false;
54   }
55 
56  protected:
CreateAlternateNavInfoBar()57   void CreateAlternateNavInfoBar() override { *displayed_infobar_ = true; }
58 
59  private:
60   // True if CreateAlternateNavInfoBar was called.  This cannot be kept in
61   // memory within this class because this class is automatically deleted when
62   // all fetchers finish (before the test can query this value), hence the
63   // pointer.
64   bool* displayed_infobar_;
65 
66   DISALLOW_COPY_AND_ASSIGN(MockChromeOmniboxNavigationObserver);
67 };
68 
69 class ChromeOmniboxNavigationObserverTest
70     : public ChromeRenderViewHostTestHarness {
71  protected:
ChromeOmniboxNavigationObserverTest()72   ChromeOmniboxNavigationObserverTest() {}
~ChromeOmniboxNavigationObserverTest()73   ~ChromeOmniboxNavigationObserverTest() override {}
74 
navigation_controller()75   content::NavigationController* navigation_controller() {
76     return &(web_contents()->GetController());
77   }
78 
model()79   TemplateURLService* model() {
80     return TemplateURLServiceFactory::GetForProfile(profile());
81   }
82 
83   // Functions that return the name of certain search keywords that are part
84   // of the TemplateURLService attached to this profile.
auto_generated_search_keyword()85   static base::string16 auto_generated_search_keyword() {
86     return base::ASCIIToUTF16("auto_generated_search_keyword");
87   }
non_auto_generated_search_keyword()88   static base::string16 non_auto_generated_search_keyword() {
89     return base::ASCIIToUTF16("non_auto_generated_search_keyword");
90   }
default_search_keyword()91   static base::string16 default_search_keyword() {
92     return base::ASCIIToUTF16("default_search_keyword");
93   }
prepopulated_search_keyword()94   static base::string16 prepopulated_search_keyword() {
95     return base::ASCIIToUTF16("prepopulated_search_keyword");
96   }
policy_search_keyword()97   static base::string16 policy_search_keyword() {
98     return base::ASCIIToUTF16("policy_search_keyword");
99   }
100 
101  private:
102   // ChromeRenderViewHostTestHarness:
103   void SetUp() override;
104 
105   DISALLOW_COPY_AND_ASSIGN(ChromeOmniboxNavigationObserverTest);
106 };
107 
SetUp()108 void ChromeOmniboxNavigationObserverTest::SetUp() {
109   ChromeRenderViewHostTestHarness::SetUp();
110   InfoBarService::CreateForWebContents(web_contents());
111 
112   // Set up a series of search engines for later testing.
113   TemplateURLServiceFactoryTestUtil factory_util(profile());
114   factory_util.VerifyLoad();
115 
116   TemplateURLData auto_gen_turl;
117   auto_gen_turl.SetKeyword(auto_generated_search_keyword());
118   auto_gen_turl.safe_for_autoreplace = true;
119   factory_util.model()->Add(std::make_unique<TemplateURL>(auto_gen_turl));
120 
121   TemplateURLData non_auto_gen_turl;
122   non_auto_gen_turl.SetKeyword(non_auto_generated_search_keyword());
123   factory_util.model()->Add(std::make_unique<TemplateURL>(non_auto_gen_turl));
124 
125   TemplateURLData default_turl;
126   default_turl.SetKeyword(default_search_keyword());
127   factory_util.model()->SetUserSelectedDefaultSearchProvider(
128       factory_util.model()->Add(std::make_unique<TemplateURL>(default_turl)));
129 
130   TemplateURLData prepopulated_turl;
131   prepopulated_turl.SetKeyword(prepopulated_search_keyword());
132   prepopulated_turl.prepopulate_id = 1;
133   factory_util.model()->Add(std::make_unique<TemplateURL>(prepopulated_turl));
134 
135   TemplateURLData policy_turl;
136   policy_turl.SetKeyword(policy_search_keyword());
137   policy_turl.created_by_policy = true;
138   factory_util.model()->Add(std::make_unique<TemplateURL>(policy_turl));
139 }
140 
TEST_F(ChromeOmniboxNavigationObserverTest,LoadStateAfterPendingNavigation)141 TEST_F(ChromeOmniboxNavigationObserverTest, LoadStateAfterPendingNavigation) {
142   std::unique_ptr<ChromeOmniboxNavigationObserver> observer =
143       std::make_unique<ChromeOmniboxNavigationObserver>(
144           profile(), base::ASCIIToUTF16("test text"), AutocompleteMatch(),
145           AutocompleteMatch());
146   EXPECT_EQ(ChromeOmniboxNavigationObserver::LOAD_NOT_SEEN,
147             observer->load_state());
148 
149   std::unique_ptr<content::NavigationEntry> entry =
150       content::NavigationController::CreateNavigationEntry(
151           GURL(), content::Referrer(), base::nullopt,
152           ui::PAGE_TRANSITION_FROM_ADDRESS_BAR, false, std::string(), profile(),
153           nullptr /* blob_url_loader_factory */);
154 
155   content::NotificationService::current()->Notify(
156       content::NOTIFICATION_NAV_ENTRY_PENDING,
157       content::Source<content::NavigationController>(navigation_controller()),
158       content::Details<content::NavigationEntry>(entry.get()));
159 
160   // A pending navigation notification should synchronously update the load
161   // state to pending.
162   EXPECT_EQ(ChromeOmniboxNavigationObserver::LOAD_PENDING,
163             observer->load_state());
164 }
165 
TEST_F(ChromeOmniboxNavigationObserverTest,DeleteBrokenCustomSearchEngines)166 TEST_F(ChromeOmniboxNavigationObserverTest, DeleteBrokenCustomSearchEngines) {
167   struct TestData {
168     base::string16 keyword;
169     int status_code;
170     bool expect_exists;
171   };
172   std::vector<TestData> cases = {
173       {auto_generated_search_keyword(), 200, true},
174       {auto_generated_search_keyword(), 404, false},
175       {non_auto_generated_search_keyword(), 404, true},
176       {default_search_keyword(), 404, true},
177       {prepopulated_search_keyword(), 404, true},
178       {policy_search_keyword(), 404, true}};
179 
180   base::string16 query = base::ASCIIToUTF16(" text");
181   for (size_t i = 0; i < cases.size(); ++i) {
182     SCOPED_TRACE("case #" + base::NumberToString(i));
183     // The keyword should always exist at the beginning.
184     EXPECT_TRUE(model()->GetTemplateURLForKeyword(cases[i].keyword));
185 
186     AutocompleteMatch match;
187     match.keyword = cases[i].keyword;
188     // |observer| gets deleted by observer->NavigationEntryCommitted().
189     ChromeOmniboxNavigationObserver* observer =
190         new ChromeOmniboxNavigationObserver(profile(), cases[i].keyword + query,
191                                             match, AutocompleteMatch());
192     auto navigation_entry =
193         content::NavigationController::CreateNavigationEntry(
194             GURL(), content::Referrer(), base::nullopt,
195             ui::PAGE_TRANSITION_FROM_ADDRESS_BAR, false, std::string(),
196             profile(), nullptr /* blob_url_loader_factory */);
197     content::LoadCommittedDetails details;
198     details.http_status_code = cases[i].status_code;
199     details.entry = navigation_entry.get();
200     observer->NavigationEntryCommitted(details);
201     EXPECT_EQ(cases[i].expect_exists,
202               model()->GetTemplateURLForKeyword(cases[i].keyword) != nullptr);
203   }
204 
205   // Also run a URL navigation that results in a 404 through the system to make
206   // sure nothing crashes for regular URL navigations.
207   // |observer| gets deleted by observer->NavigationEntryCommitted().
208   ChromeOmniboxNavigationObserver* observer =
209       new ChromeOmniboxNavigationObserver(
210           profile(), base::ASCIIToUTF16("url navigation"), AutocompleteMatch(),
211           AutocompleteMatch());
212   auto navigation_entry = content::NavigationController::CreateNavigationEntry(
213       GURL(), content::Referrer(), base::nullopt,
214       ui::PAGE_TRANSITION_FROM_ADDRESS_BAR, false, std::string(), profile(),
215       nullptr /* blob_url_loader_factory */);
216   content::LoadCommittedDetails details;
217   details.http_status_code = 404;
218   details.entry = navigation_entry.get();
219   observer->NavigationEntryCommitted(details);
220 }
221 
TEST_F(ChromeOmniboxNavigationObserverTest,AlternateNavInfoBar)222 TEST_F(ChromeOmniboxNavigationObserverTest, AlternateNavInfoBar) {
223   TestURLLoaderFactory test_url_loader_factory;
224   scoped_refptr<network::SharedURLLoaderFactory> shared_factory =
225       base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
226           &test_url_loader_factory);
227 
228   const int kNetError = 0;
229   const int kNoResponse = -1;
230   struct Response {
231     const std::vector<std::string> urls;  // If more than one, 301 between them.
232     // The final status code to return after all the redirects, or one of
233     // kNetError or kNoResponse.
234     const int http_response_code;
235     std::string content;
236   };
237 
238   // All of these test cases assume the alternate nav URL is http://example/.
239   struct Case {
240     const Response response;
241     const bool expected_alternate_nav_bar_shown;
242   } cases[] = {
243       // The only response provided is a net error.
244       {{{"http://example/"}, kNetError}, false},
245       // The response connected to a valid page.
246       {{{"http://example/"}, 200}, true},
247       // A non-empty page, despite the HEAD.
248       {{{"http://example/"}, 200, "Content"}, true},
249       // The response connected to an error page.
250       {{{"http://example/"}, 404}, false},
251 
252       // The response redirected to same host, just http->https, with a path
253       // change as well.  In this case the second URL should not fetched; Chrome
254       // will optimistically assume the destination will return a valid page and
255       // display the infobar.
256       {{{"http://example/", "https://example/path"}, kNoResponse}, true},
257       // Ditto, making sure it still holds when the final destination URL
258       // returns a valid status code.
259       {{{"http://example/", "https://example/path"}, 200}, true},
260 
261       // The response redirected to an entirely different host.  In these cases,
262       // no URL should be fetched against this second host; again Chrome will
263       // optimistically assume the destination will return a valid page and
264       // display the infobar.
265       {{{"http://example/", "http://new-destination/"}, kNoResponse}, true},
266       // Ditto, making sure it still holds when the final destination URL
267       // returns a valid status code.
268       {{{"http://example/", "http://new-destination/"}, 200}, true},
269 
270       // The response redirected to same host, just http->https, with no other
271       // changes.  In these cases, Chrome will fetch the second URL.
272       // The second URL response returned a valid page.
273       {{{"http://example/", "https://example/"}, 200}, true},
274       // The second URL response returned an error page.
275       {{{"http://example/", "https://example/"}, 404}, false},
276       // The second URL response returned a net error.
277       {{{"http://example/", "https://example/"}, kNetError}, false},
278       // The second URL response redirected again.
279       {{{"http://example/", "https://example/", "https://example/root"},
280         kNoResponse},
281        true},
282   };
283   for (size_t i = 0; i < base::size(cases); ++i) {
284     SCOPED_TRACE("case #" + base::NumberToString(i));
285     const Case& test_case = cases[i];
286     const Response& response = test_case.response;
287 
288     // Set the URL request responses.
289     test_url_loader_factory.ClearResponses();
290 
291     // Compute URL redirect chain.
292     TestURLLoaderFactory::Redirects redirects;
293     for (size_t dest = 1; dest < response.urls.size(); ++dest) {
294       net::RedirectInfo redir_info;
295       redir_info.new_url = GURL(response.urls[dest]);
296       redir_info.status_code = net::HTTP_MOVED_PERMANENTLY;
297       auto redir_head =
298           network::CreateURLResponseHead(net::HTTP_MOVED_PERMANENTLY);
299       redirects.push_back({redir_info, std::move(redir_head)});
300     }
301 
302     // Fill in final response.
303     network::mojom::URLResponseHeadPtr http_head =
304         network::mojom::URLResponseHead::New();
305     network::URLLoaderCompletionStatus net_status;
306     network::TestURLLoaderFactory::ResponseProduceFlags response_flags =
307         network::TestURLLoaderFactory::kResponseDefault;
308 
309     if (response.http_response_code == kNoResponse) {
310       response_flags =
311           TestURLLoaderFactory::kResponseOnlyRedirectsNoDestination;
312     } else if (response.http_response_code == kNetError) {
313       net_status = network::URLLoaderCompletionStatus(net::ERR_FAILED);
314     } else {
315       net_status = network::URLLoaderCompletionStatus(net::OK);
316       http_head = network::CreateURLResponseHead(
317           static_cast<net::HttpStatusCode>(response.http_response_code));
318     }
319 
320     test_url_loader_factory.AddResponse(GURL(response.urls[0]),
321                                         std::move(http_head), response.content,
322                                         net_status, std::move(redirects));
323 
324     // Create the alternate nav match and the observer.
325     // |observer| gets deleted automatically after all fetchers complete.
326     AutocompleteMatch alternate_nav_match;
327     alternate_nav_match.destination_url = GURL("http://example/");
328     bool displayed_infobar;
329     ChromeOmniboxNavigationObserver* observer =
330         new MockChromeOmniboxNavigationObserver(
331             profile(), base::ASCIIToUTF16("example"), AutocompleteMatch(),
332             alternate_nav_match, &displayed_infobar);
333     observer->SetURLLoaderFactoryForTesting(shared_factory);
334 
335     // Send the observer NAV_ENTRY_PENDING to get the URL fetcher to start.
336     auto navigation_entry =
337         content::NavigationController::CreateNavigationEntry(
338             GURL(), content::Referrer(), base::nullopt,
339             ui::PAGE_TRANSITION_FROM_ADDRESS_BAR, false, std::string(),
340             profile(), nullptr /* blob_url_loader_factory */);
341     content::NotificationService::current()->Notify(
342         content::NOTIFICATION_NAV_ENTRY_PENDING,
343         content::Source<content::NavigationController>(navigation_controller()),
344         content::Details<content::NavigationEntry>(navigation_entry.get()));
345 
346     // Make sure the fetcher(s) have finished.
347     base::RunLoop().RunUntilIdle();
348 
349     // Send the observer NavigationEntryCommitted() to get it to display the
350     // infobar if needed.
351     content::LoadCommittedDetails details;
352     details.http_status_code = 200;
353     details.entry = navigation_entry.get();
354     observer->NavigationEntryCommitted(details);
355 
356     // See if AlternateNavInfoBarDelegate::Create() was called.
357     EXPECT_EQ(test_case.expected_alternate_nav_bar_shown, displayed_infobar);
358   }
359 }
360