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 "chrome/common/importer/firefox_importer_utils.h"
6
7#include <algorithm>
8#include <map>
9#include <string>
10
11#include "base/files/file_util.h"
12#include "base/logging.h"
13#include "base/strings/string_number_conversions.h"
14#include "base/strings/string_split.h"
15#include "base/strings/string_util.h"
16#include "base/strings/stringprintf.h"
17#include "base/strings/utf_string_conversions.h"
18#include "base/values.h"
19#include "chrome/common/ini_parser.h"
20#include "chrome/grit/generated_resources.h"
21#include "ui/base/l10n/l10n_util.h"
22#include "url/gurl.h"
23
24namespace {
25
26// Retrieves the file system path of the profile name.
27base::FilePath GetProfilePath(const base::DictionaryValue& root,
28                              const std::string& profile_name) {
29  base::string16 path16;
30  std::string is_relative;
31  if (!root.GetStringASCII(profile_name + ".IsRelative", &is_relative) ||
32      !root.GetString(profile_name + ".Path", &path16))
33    return base::FilePath();
34
35#if defined(OS_WIN)
36  ReplaceSubstringsAfterOffset(
37      &path16, 0, base::ASCIIToUTF16("/"), base::ASCIIToUTF16("\\"));
38#endif
39  base::FilePath path = base::FilePath::FromUTF16Unsafe(path16);
40
41  // IsRelative=1 means the folder path would be relative to the
42  // path of profiles.ini. IsRelative=0 refers to a custom profile
43  // location.
44  if (is_relative == "1")
45    path = GetProfilesINI().DirName().Append(path);
46
47  return path;
48}
49
50// Checks if the named profile is the default profile.
51bool IsDefaultProfile(const base::DictionaryValue& root,
52                      const std::string& profile_name) {
53  std::string is_default;
54  root.GetStringASCII(profile_name + ".Default", &is_default);
55  return is_default == "1";
56}
57
58} // namespace
59
60base::FilePath GetFirefoxProfilePath() {
61  base::FilePath ini_file = GetProfilesINI();
62  std::string content;
63  base::ReadFileToString(ini_file, &content);
64  DictionaryValueINIParser ini_parser;
65  ini_parser.Parse(content);
66  return GetFirefoxProfilePathFromDictionary(ini_parser.root());
67}
68
69base::FilePath GetFirefoxProfilePathFromDictionary(
70    const base::DictionaryValue& root) {
71  std::vector<std::string> profiles;
72  for (int i = 0; ; ++i) {
73    std::string current_profile = base::StringPrintf("Profile%d", i);
74    if (root.HasKey(current_profile)) {
75      profiles.push_back(current_profile);
76    } else {
77      // Profiles are continuously numbered. So we exit when we can't
78      // find the i-th one.
79      break;
80    }
81  }
82
83  if (profiles.empty())
84    return base::FilePath();
85
86  // When multiple profiles exist, the path to the default profile is returned,
87  // since the other profiles are used mostly by developers for testing.
88  for (std::vector<std::string>::const_iterator it = profiles.begin();
89       it != profiles.end(); ++it)
90    if (IsDefaultProfile(root, *it))
91      return GetProfilePath(root, *it);
92
93  // If no default profile is found, the path to Profile0 will be returned.
94  return GetProfilePath(root, profiles.front());
95}
96
97#if defined(OS_MACOSX)
98// Find the "*.app" component of the path and build up from there.
99// The resulting path will be .../Firefox.app/Contents/MacOS.
100// We do this because we don't trust LastAppDir to always be
101// this particular path, without any subdirs, and we want to make
102// our assumption about Firefox's root being in that path explicit.
103bool ComposeMacAppPath(const std::string& path_from_file,
104                       base::FilePath* output) {
105  base::FilePath path(path_from_file);
106  typedef std::vector<base::FilePath::StringType> ComponentVector;
107  ComponentVector path_components;
108  path.GetComponents(&path_components);
109  if (path_components.empty())
110    return false;
111  // The first path component is special because it may be absolute. Calling
112  // Append with an absolute path component will trigger an assert, so we
113  // must handle it differently and initialize output with it.
114  *output = base::FilePath(path_components[0]);
115  // Append next path components untill we find the *.app component. When we do,
116  // append Contents/MacOS.
117  for (size_t i = 1; i < path_components.size(); ++i) {
118    *output = output->Append(path_components[i]);
119    if (EndsWith(path_components[i], ".app", true)) {
120      *output = output->Append("Contents");
121      *output = output->Append("MacOS");
122      return true;
123    }
124  }
125  LOG(ERROR) << path_from_file << " doesn't look like a valid Firefox "
126             << "installation path: missing /*.app/ directory.";
127  return false;
128}
129#endif  // OS_MACOSX
130
131bool GetFirefoxVersionAndPathFromProfile(const base::FilePath& profile_path,
132                                         int* version,
133                                         base::FilePath* app_path) {
134  bool ret = false;
135  base::FilePath compatibility_file =
136      profile_path.AppendASCII("compatibility.ini");
137  std::string content;
138  base::ReadFileToString(compatibility_file, &content);
139  ReplaceSubstringsAfterOffset(&content, 0, "\r\n", "\n");
140  std::vector<std::string> lines;
141  base::SplitString(content, '\n', &lines);
142
143  for (size_t i = 0; i < lines.size(); ++i) {
144    const std::string& line = lines[i];
145    if (line.empty() || line[0] == '#' || line[0] == ';')
146      continue;
147    size_t equal = line.find('=');
148    if (equal != std::string::npos) {
149      std::string key = line.substr(0, equal);
150      if (key == "LastVersion") {
151        base::StringToInt(line.substr(equal + 1), version);
152        ret = true;
153      } else if (key == "LastAppDir") {
154        // TODO(evanm): If the path in question isn't convertible to
155        // UTF-8, what does Firefox do?  If it puts raw bytes in the
156        // file, we could go straight from bytes -> filepath;
157        // otherwise, we're out of luck here.
158#if defined(OS_MACOSX)
159        // Extract path from "LastAppDir=/actual/path"
160        size_t separator_pos = line.find_first_of('=');
161        const std::string& path_from_ini = line.substr(separator_pos + 1);
162        if (!ComposeMacAppPath(path_from_ini, app_path))
163          return false;
164#else  // !OS_MACOSX
165        *app_path = base::FilePath::FromUTF8Unsafe(line.substr(equal + 1));
166#endif  // OS_MACOSX
167      }
168    }
169  }
170  return ret;
171}
172
173bool ReadPrefFile(const base::FilePath& path, std::string* content) {
174  if (content == NULL)
175    return false;
176
177  base::ReadFileToString(path, content);
178
179  if (content->empty()) {
180    LOG(WARNING) << "Firefox preference file " << path.value() << " is empty.";
181    return false;
182  }
183
184  return true;
185}
186
187std::string ReadBrowserConfigProp(const base::FilePath& app_path,
188                                  const std::string& pref_key) {
189  std::string content;
190  if (!ReadPrefFile(app_path.AppendASCII("browserconfig.properties"), &content))
191    return std::string();
192
193  // This file has the syntax: key=value.
194  size_t prop_index = content.find(pref_key + "=");
195  if (prop_index == std::string::npos)
196    return std::string();
197
198  size_t start = prop_index + pref_key.length();
199  size_t stop = std::string::npos;
200  if (start != std::string::npos)
201    stop = content.find("\n", start + 1);
202
203  if (start == std::string::npos ||
204      stop == std::string::npos || (start == stop)) {
205    LOG(WARNING) << "Firefox property " << pref_key << " could not be parsed.";
206    return std::string();
207  }
208
209  return content.substr(start + 1, stop - start - 1);
210}
211
212std::string ReadPrefsJsValue(const base::FilePath& profile_path,
213                             const std::string& pref_key) {
214  std::string content;
215  if (!ReadPrefFile(profile_path.AppendASCII("prefs.js"), &content))
216    return std::string();
217
218  return GetPrefsJsValue(content, pref_key);
219}
220
221GURL GetHomepage(const base::FilePath& profile_path) {
222  std::string home_page_list =
223      ReadPrefsJsValue(profile_path, "browser.startup.homepage");
224
225  size_t seperator = home_page_list.find_first_of('|');
226  if (seperator == std::string::npos)
227    return GURL(home_page_list);
228
229  return GURL(home_page_list.substr(0, seperator));
230}
231
232bool IsDefaultHomepage(const GURL& homepage, const base::FilePath& app_path) {
233  if (!homepage.is_valid())
234    return false;
235
236  std::string default_homepages =
237      ReadBrowserConfigProp(app_path, "browser.startup.homepage");
238
239  size_t seperator = default_homepages.find_first_of('|');
240  if (seperator == std::string::npos)
241    return homepage.spec() == GURL(default_homepages).spec();
242
243  // Crack the string into separate homepage urls.
244  std::vector<std::string> urls;
245  base::SplitString(default_homepages, '|', &urls);
246
247  for (size_t i = 0; i < urls.size(); ++i) {
248    if (homepage.spec() == GURL(urls[i]).spec())
249      return true;
250  }
251
252  return false;
253}
254
255std::string GetPrefsJsValue(const std::string& content,
256                            const std::string& pref_key) {
257  // This file has the syntax: user_pref("key", value);
258  std::string search_for = std::string("user_pref(\"") + pref_key +
259                           std::string("\", ");
260  size_t prop_index = content.find(search_for);
261  if (prop_index == std::string::npos)
262    return std::string();
263
264  size_t start = prop_index + search_for.length();
265  size_t stop = std::string::npos;
266  if (start != std::string::npos) {
267    // Stop at the last ')' on this line.
268    stop = content.find("\n", start + 1);
269    stop = content.rfind(")", stop);
270  }
271
272  if (start == std::string::npos || stop == std::string::npos ||
273      stop < start) {
274    LOG(WARNING) << "Firefox property " << pref_key << " could not be parsed.";
275    return std::string();
276  }
277
278  // String values have double quotes we don't need to return to the caller.
279  if (content[start] == '\"' && content[stop - 1] == '\"') {
280    ++start;
281    --stop;
282  }
283
284  return content.substr(start, stop - start);
285}
286
287// The branding name is obtained from the application.ini file from the Firefox
288// application directory. A sample application.ini file is the following:
289//   [App]
290//   Vendor=Mozilla
291//   Name=Iceweasel
292//   Profile=mozilla/firefox
293//   Version=3.5.16
294//   BuildID=20120421070307
295//   Copyright=Copyright (c) 1998 - 2010 mozilla.org
296//   ID={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
297//   .........................................
298// In this example the function returns "Iceweasel" (or a localized equivalent).
299base::string16 GetFirefoxImporterName(const base::FilePath& app_path) {
300  const base::FilePath app_ini_file = app_path.AppendASCII("application.ini");
301  std::string branding_name;
302  if (base::PathExists(app_ini_file)) {
303    std::string content;
304    base::ReadFileToString(app_ini_file, &content);
305    std::vector<std::string> lines;
306    base::SplitString(content, '\n', &lines);
307    const std::string name_attr("Name=");
308    bool in_app_section = false;
309    for (size_t i = 0; i < lines.size(); ++i) {
310      base::TrimWhitespace(lines[i], base::TRIM_ALL, &lines[i]);
311      if (lines[i] == "[App]") {
312        in_app_section = true;
313      } else if (in_app_section) {
314        if (lines[i].find(name_attr) == 0) {
315          branding_name = lines[i].substr(name_attr.size());
316          break;
317        } else if (lines[i].length() > 0 && lines[i][0] == '[') {
318          // No longer in the [App] section.
319          break;
320        }
321      }
322    }
323  }
324
325  base::StringToLowerASCII(&branding_name);
326  if (branding_name.find("iceweasel") != std::string::npos)
327    return l10n_util::GetStringUTF16(IDS_IMPORT_FROM_ICEWEASEL);
328  return l10n_util::GetStringUTF16(IDS_IMPORT_FROM_FIREFOX);
329}
330