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 "content/public/browser/host_zoom_map.h"
6
7#include <algorithm>
8#include <string>
9#include <vector>
10
11#include "base/bind.h"
12#include "base/file_util.h"
13#include "base/files/file_path.h"
14#include "base/memory/ref_counted.h"
15#include "base/memory/scoped_ptr.h"
16#include "base/path_service.h"
17#include "base/prefs/pref_service.h"
18#include "base/strings/stringprintf.h"
19#include "base/values.h"
20#include "chrome/browser/chrome_page_zoom.h"
21#include "chrome/browser/profiles/profile.h"
22#include "chrome/browser/ui/browser.h"
23#include "chrome/browser/ui/tabs/tab_strip_model.h"
24#include "chrome/common/chrome_constants.h"
25#include "chrome/common/chrome_paths.h"
26#include "chrome/common/pref_names.h"
27#include "chrome/test/base/in_process_browser_test.h"
28#include "chrome/test/base/testing_profile.h"
29#include "chrome/test/base/ui_test_utils.h"
30#include "content/public/test/test_utils.h"
31#include "net/dns/mock_host_resolver.h"
32#include "net/test/embedded_test_server/embedded_test_server.h"
33#include "net/test/embedded_test_server/http_response.h"
34#include "testing/gmock/include/gmock/gmock.h"
35#include "url/gurl.h"
36
37namespace {
38
39class ZoomLevelChangeObserver {
40 public:
41  explicit ZoomLevelChangeObserver(Profile* profile)
42      : message_loop_runner_(new content::MessageLoopRunner) {
43    content::HostZoomMap* host_zoom_map = static_cast<content::HostZoomMap*>(
44        content::HostZoomMap::GetForBrowserContext(profile));
45    subscription_ = host_zoom_map->AddZoomLevelChangedCallback(base::Bind(
46        &ZoomLevelChangeObserver::OnZoomLevelChanged, base::Unretained(this)));
47  }
48
49  void BlockUntilZoomLevelForHostHasChanged(const std::string& host) {
50    while (!std::count(changed_hosts_.begin(), changed_hosts_.end(), host)) {
51      message_loop_runner_->Run();
52      message_loop_runner_ = new content::MessageLoopRunner;
53    }
54    changed_hosts_.clear();
55  }
56
57 private:
58  void OnZoomLevelChanged(const content::HostZoomMap::ZoomLevelChange& change) {
59    changed_hosts_.push_back(change.host);
60    message_loop_runner_->Quit();
61  }
62
63  scoped_refptr<content::MessageLoopRunner> message_loop_runner_;
64  std::vector<std::string> changed_hosts_;
65  scoped_ptr<content::HostZoomMap::Subscription> subscription_;
66
67  DISALLOW_COPY_AND_ASSIGN(ZoomLevelChangeObserver);
68};
69
70}  // namespace
71
72class HostZoomMapBrowserTest : public InProcessBrowserTest {
73 public:
74  HostZoomMapBrowserTest() {}
75
76 protected:
77  void SetDefaultZoomLevel(double level) {
78    browser()->profile()->GetPrefs()->SetDouble(
79        prefs::kDefaultZoomLevel, level);
80  }
81
82  double GetZoomLevel(const GURL& url) {
83    content::HostZoomMap* host_zoom_map = static_cast<content::HostZoomMap*>(
84        content::HostZoomMap::GetForBrowserContext(browser()->profile()));
85    return host_zoom_map->GetZoomLevelForHostAndScheme(url.scheme(),
86                                                       url.host());
87  }
88
89  std::vector<std::string> GetHostsWithZoomLevels() {
90    typedef content::HostZoomMap::ZoomLevelVector ZoomLevelVector;
91    content::HostZoomMap* host_zoom_map = static_cast<content::HostZoomMap*>(
92        content::HostZoomMap::GetForBrowserContext(browser()->profile()));
93    content::HostZoomMap::ZoomLevelVector zoom_levels =
94        host_zoom_map->GetAllZoomLevels();
95    std::vector<std::string> results;
96    for (ZoomLevelVector::const_iterator it = zoom_levels.begin();
97         it != zoom_levels.end(); ++it)
98      results.push_back(it->host);
99    return results;
100  }
101
102  std::vector<std::string> GetHostsWithZoomLevelsFromPrefs() {
103    PrefService* prefs = browser()->profile()->GetPrefs();
104    const base::DictionaryValue* values =
105        prefs->GetDictionary(prefs::kPerHostZoomLevels);
106    std::vector<std::string> results;
107    if (values) {
108      for (base::DictionaryValue::Iterator it(*values);
109           !it.IsAtEnd(); it.Advance())
110        results.push_back(it.key());
111    }
112    return results;
113  }
114
115  GURL ConstructTestServerURL(const char* url_template) {
116    return GURL(base::StringPrintf(
117        url_template, embedded_test_server()->port()));
118  }
119
120 private:
121  scoped_ptr<net::test_server::HttpResponse> HandleRequest(
122      const net::test_server::HttpRequest& request) {
123    return scoped_ptr<net::test_server::HttpResponse>(
124        new net::test_server::BasicHttpResponse);
125  }
126
127  // BrowserTestBase:
128  virtual void SetUpOnMainThread() OVERRIDE {
129    ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady());
130    embedded_test_server()->RegisterRequestHandler(base::Bind(
131        &HostZoomMapBrowserTest::HandleRequest, base::Unretained(this)));
132    host_resolver()->AddRule("*", "127.0.0.1");
133  }
134
135  DISALLOW_COPY_AND_ASSIGN(HostZoomMapBrowserTest);
136};
137
138class HostZoomMapSanitizationBrowserTest : public HostZoomMapBrowserTest {
139 public:
140  HostZoomMapSanitizationBrowserTest() {}
141
142 private:
143  // InProcessBrowserTest:
144  virtual bool SetUpUserDataDirectory() OVERRIDE {
145    // Zoom-related preferences demonstrating the two problems that could be
146    // caused by the bug. They incorrectly contain a per-host zoom level for the
147    // empty host; and a value for 'host1' that only differs from the default by
148    // epsilon. Neither should have been persisted.
149    const char kBrokenPrefs[] =
150        "{'profile': {"
151        "   'default_zoom_level': 1.2,"
152        "   'per_host_zoom_levels': {'': 1.1, 'host1': 1.20001, 'host2': 1.3}"
153        "}}";
154    std::string broken_prefs(kBrokenPrefs);
155    std::replace(broken_prefs.begin(), broken_prefs.end(), '\'', '\"');
156
157    base::FilePath user_data_directory, path_to_prefs;
158    PathService::Get(chrome::DIR_USER_DATA, &user_data_directory);
159    path_to_prefs = user_data_directory
160        .AppendASCII(TestingProfile::kTestUserProfileDir)
161        .Append(chrome::kPreferencesFilename);
162    base::CreateDirectory(path_to_prefs.DirName());
163    base::WriteFile(path_to_prefs, broken_prefs.c_str(), broken_prefs.size());
164    return true;
165  }
166
167  DISALLOW_COPY_AND_ASSIGN(HostZoomMapSanitizationBrowserTest);
168};
169
170// Regression test for crbug.com/364399.
171IN_PROC_BROWSER_TEST_F(HostZoomMapBrowserTest, ToggleDefaultZoomLevel) {
172  const double default_zoom_level = content::ZoomFactorToZoomLevel(1.5);
173
174  const char kTestURLTemplate1[] = "http://host1:%d/";
175  const char kTestURLTemplate2[] = "http://host2:%d/";
176
177  ZoomLevelChangeObserver observer(browser()->profile());
178
179  GURL test_url1 = ConstructTestServerURL(kTestURLTemplate1);
180  ui_test_utils::NavigateToURL(browser(), test_url1);
181
182  SetDefaultZoomLevel(default_zoom_level);
183  observer.BlockUntilZoomLevelForHostHasChanged(test_url1.host());
184  EXPECT_TRUE(
185      content::ZoomValuesEqual(default_zoom_level, GetZoomLevel(test_url1)));
186
187  GURL test_url2 = ConstructTestServerURL(kTestURLTemplate2);
188  ui_test_utils::NavigateToURLWithDisposition(
189      browser(), test_url2, NEW_FOREGROUND_TAB,
190      ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION);
191  EXPECT_TRUE(
192      content::ZoomValuesEqual(default_zoom_level, GetZoomLevel(test_url2)));
193
194  content::WebContents* web_contents =
195      browser()->tab_strip_model()->GetActiveWebContents();
196  chrome_page_zoom::Zoom(web_contents, content::PAGE_ZOOM_OUT);
197  observer.BlockUntilZoomLevelForHostHasChanged(test_url2.host());
198  EXPECT_FALSE(
199      content::ZoomValuesEqual(default_zoom_level, GetZoomLevel(test_url2)));
200
201  chrome_page_zoom::Zoom(web_contents, content::PAGE_ZOOM_IN);
202  observer.BlockUntilZoomLevelForHostHasChanged(test_url2.host());
203  EXPECT_TRUE(
204      content::ZoomValuesEqual(default_zoom_level, GetZoomLevel(test_url2)));
205
206  // Now both tabs should be at the default zoom level, so there should not be
207  // any per-host values saved either to Pref, or internally in HostZoomMap.
208  EXPECT_TRUE(GetHostsWithZoomLevels().empty());
209  EXPECT_TRUE(GetHostsWithZoomLevelsFromPrefs().empty());
210}
211
212// Test that garbage data from crbug.com/364399 is cleared up on startup.
213IN_PROC_BROWSER_TEST_F(HostZoomMapSanitizationBrowserTest, ClearOnStartup) {
214  EXPECT_THAT(GetHostsWithZoomLevels(), testing::ElementsAre("host2"));
215  EXPECT_THAT(GetHostsWithZoomLevelsFromPrefs(), testing::ElementsAre("host2"));
216}
217