1// Copyright (c) 2011 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/importer/firefox_importer_utils.h"
6
7#include <algorithm>
8#include <map>
9#include <string>
10
11#include "base/file_util.h"
12#include "base/logging.h"
13#include "base/string_split.h"
14#include "base/string_util.h"
15#include "base/string_number_conversions.h"
16#include "base/utf_string_conversions.h"
17#include "base/values.h"
18#include "chrome/browser/search_engines/template_url.h"
19#include "chrome/browser/search_engines/template_url_model.h"
20#include "chrome/browser/search_engines/template_url_parser.h"
21#include "chrome/browser/search_engines/template_url_prepopulate_data.h"
22#include "googleurl/src/gurl.h"
23
24namespace {
25
26// FirefoxURLParameterFilter is used to remove parameter mentioning Firefox from
27// the search URL when importing search engines.
28class FirefoxURLParameterFilter : public TemplateURLParser::ParameterFilter {
29 public:
30  FirefoxURLParameterFilter() {}
31  virtual ~FirefoxURLParameterFilter() {}
32
33  // TemplateURLParser::ParameterFilter method.
34  virtual bool KeepParameter(const std::string& key,
35                             const std::string& value) {
36    std::string low_value = StringToLowerASCII(value);
37    if (low_value.find("mozilla") != std::string::npos ||
38        low_value.find("firefox") != std::string::npos ||
39        low_value.find("moz:") != std::string::npos )
40      return false;
41    return true;
42  }
43
44 private:
45  DISALLOW_COPY_AND_ASSIGN(FirefoxURLParameterFilter);
46};
47}  // namespace
48
49FilePath GetFirefoxProfilePath() {
50  DictionaryValue root;
51  FilePath ini_file = GetProfilesINI();
52  ParseProfileINI(ini_file, &root);
53
54  FilePath source_path;
55  for (int i = 0; ; ++i) {
56    std::string current_profile = StringPrintf("Profile%d", i);
57    if (!root.HasKey(current_profile)) {
58      // Profiles are continuously numbered. So we exit when we can't
59      // find the i-th one.
60      break;
61    }
62    std::string is_relative;
63    string16 path16;
64    if (root.GetStringASCII(current_profile + ".IsRelative", &is_relative) &&
65        root.GetString(current_profile + ".Path", &path16)) {
66#if defined(OS_WIN)
67      ReplaceSubstringsAfterOffset(
68          &path16, 0, ASCIIToUTF16("/"), ASCIIToUTF16("\\"));
69#endif
70      FilePath path = FilePath::FromWStringHack(UTF16ToWide(path16));
71
72      // IsRelative=1 means the folder path would be relative to the
73      // path of profiles.ini. IsRelative=0 refers to a custom profile
74      // location.
75      if (is_relative == "1") {
76        path = ini_file.DirName().Append(path);
77      }
78
79      // We only import the default profile when multiple profiles exist,
80      // since the other profiles are used mostly by developers for testing.
81      // Otherwise, Profile0 will be imported.
82      std::string is_default;
83      if ((root.GetStringASCII(current_profile + ".Default", &is_default) &&
84           is_default == "1") || i == 0) {
85        // We have found the default profile.
86        return path;
87      }
88    }
89  }
90  return FilePath();
91}
92
93
94bool GetFirefoxVersionAndPathFromProfile(const FilePath& profile_path,
95                                         int* version,
96                                         FilePath* app_path) {
97  bool ret = false;
98  FilePath compatibility_file = profile_path.AppendASCII("compatibility.ini");
99  std::string content;
100  file_util::ReadFileToString(compatibility_file, &content);
101  ReplaceSubstringsAfterOffset(&content, 0, "\r\n", "\n");
102  std::vector<std::string> lines;
103  base::SplitString(content, '\n', &lines);
104
105  for (size_t i = 0; i < lines.size(); ++i) {
106    const std::string& line = lines[i];
107    if (line.empty() || line[0] == '#' || line[0] == ';')
108      continue;
109    size_t equal = line.find('=');
110    if (equal != std::string::npos) {
111      std::string key = line.substr(0, equal);
112      if (key == "LastVersion") {
113        *version = line.substr(equal + 1)[0] - '0';
114        ret = true;
115      } else if (key == "LastAppDir") {
116        // TODO(evanm): If the path in question isn't convertible to
117        // UTF-8, what does Firefox do?  If it puts raw bytes in the
118        // file, we could go straight from bytes -> filepath;
119        // otherwise, we're out of luck here.
120        *app_path = FilePath::FromWStringHack(
121            UTF8ToWide(line.substr(equal + 1)));
122      }
123    }
124  }
125  return ret;
126}
127
128void ParseProfileINI(const FilePath& file, DictionaryValue* root) {
129  // Reads the whole INI file.
130  std::string content;
131  file_util::ReadFileToString(file, &content);
132  ReplaceSubstringsAfterOffset(&content, 0, "\r\n", "\n");
133  std::vector<std::string> lines;
134  base::SplitString(content, '\n', &lines);
135
136  // Parses the file.
137  root->Clear();
138  std::string current_section;
139  for (size_t i = 0; i < lines.size(); ++i) {
140    std::string line = lines[i];
141    if (line.empty()) {
142      // Skips the empty line.
143      continue;
144    }
145    if (line[0] == '#' || line[0] == ';') {
146      // This line is a comment.
147      continue;
148    }
149    if (line[0] == '[') {
150      // It is a section header.
151      current_section = line.substr(1);
152      size_t end = current_section.rfind(']');
153      if (end != std::string::npos)
154        current_section.erase(end);
155    } else {
156      std::string key, value;
157      size_t equal = line.find('=');
158      if (equal != std::string::npos) {
159        key = line.substr(0, equal);
160        value = line.substr(equal + 1);
161        // Checks whether the section and key contain a '.' character.
162        // Those sections and keys break DictionaryValue's path format,
163        // so we discard them.
164        if (current_section.find('.') == std::string::npos &&
165            key.find('.') == std::string::npos)
166          root->SetString(current_section + "." + key, value);
167      }
168    }
169  }
170}
171
172bool CanImportURL(const GURL& url) {
173  const char* kInvalidSchemes[] = {"wyciwyg", "place", "about", "chrome"};
174
175  // The URL is not valid.
176  if (!url.is_valid())
177    return false;
178
179  // Filter out the URLs with unsupported schemes.
180  for (size_t i = 0; i < arraysize(kInvalidSchemes); ++i) {
181    if (url.SchemeIs(kInvalidSchemes[i]))
182      return false;
183  }
184
185  return true;
186}
187
188void ParseSearchEnginesFromXMLFiles(const std::vector<FilePath>& xml_files,
189                                    std::vector<TemplateURL*>* search_engines) {
190  DCHECK(search_engines);
191
192  std::map<std::string, TemplateURL*> search_engine_for_url;
193  std::string content;
194  // The first XML file represents the default search engine in Firefox 3, so we
195  // need to keep it on top of the list.
196  TemplateURL* default_turl = NULL;
197  for (std::vector<FilePath>::const_iterator file_iter = xml_files.begin();
198       file_iter != xml_files.end(); ++file_iter) {
199    file_util::ReadFileToString(*file_iter, &content);
200    TemplateURL* template_url = new TemplateURL();
201    FirefoxURLParameterFilter param_filter;
202    if (TemplateURLParser::Parse(
203        reinterpret_cast<const unsigned char*>(content.data()),
204        content.length(), &param_filter, template_url) &&
205        template_url->url()) {
206      std::string url = template_url->url()->url();
207      std::map<std::string, TemplateURL*>::iterator iter =
208          search_engine_for_url.find(url);
209      if (iter != search_engine_for_url.end()) {
210        // We have already found a search engine with the same URL.  We give
211        // priority to the latest one found, as GetSearchEnginesXMLFiles()
212        // returns a vector with first Firefox default search engines and then
213        // the user's ones.  We want to give priority to the user ones.
214        delete iter->second;
215        search_engine_for_url.erase(iter);
216      }
217      // Give this a keyword to facilitate tab-to-search, if possible.
218      GURL gurl = GURL(url);
219      template_url->set_keyword(
220          TemplateURLModel::GenerateKeyword(gurl, false));
221      template_url->set_logo_id(
222          TemplateURLPrepopulateData::GetSearchEngineLogo(gurl));
223      template_url->set_show_in_default_list(true);
224      search_engine_for_url[url] = template_url;
225      if (!default_turl)
226        default_turl = template_url;
227    } else {
228      delete template_url;
229    }
230    content.clear();
231  }
232
233  // Put the results in the |search_engines| vector.
234  std::map<std::string, TemplateURL*>::iterator t_iter;
235  for (t_iter = search_engine_for_url.begin();
236       t_iter != search_engine_for_url.end(); ++t_iter) {
237    if (t_iter->second == default_turl)
238      search_engines->insert(search_engines->begin(), default_turl);
239    else
240      search_engines->push_back(t_iter->second);
241  }
242}
243
244bool ReadPrefFile(const FilePath& path, std::string* content) {
245  if (content == NULL)
246    return false;
247
248  file_util::ReadFileToString(path, content);
249
250  if (content->empty()) {
251    LOG(WARNING) << "Firefox preference file " << path.value() << " is empty.";
252    return false;
253  }
254
255  return true;
256}
257
258std::string ReadBrowserConfigProp(const FilePath& app_path,
259                                  const std::string& pref_key) {
260  std::string content;
261  if (!ReadPrefFile(app_path.AppendASCII("browserconfig.properties"), &content))
262    return "";
263
264  // This file has the syntax: key=value.
265  size_t prop_index = content.find(pref_key + "=");
266  if (prop_index == std::string::npos)
267    return "";
268
269  size_t start = prop_index + pref_key.length();
270  size_t stop = std::string::npos;
271  if (start != std::string::npos)
272    stop = content.find("\n", start + 1);
273
274  if (start == std::string::npos ||
275      stop == std::string::npos || (start == stop)) {
276    LOG(WARNING) << "Firefox property " << pref_key << " could not be parsed.";
277    return "";
278  }
279
280  return content.substr(start + 1, stop - start - 1);
281}
282
283std::string ReadPrefsJsValue(const FilePath& profile_path,
284                             const std::string& pref_key) {
285  std::string content;
286  if (!ReadPrefFile(profile_path.AppendASCII("prefs.js"), &content))
287    return "";
288
289  return GetPrefsJsValue(content, pref_key);
290}
291
292int GetFirefoxDefaultSearchEngineIndex(
293    const std::vector<TemplateURL*>& search_engines,
294    const FilePath& profile_path) {
295  // The default search engine is contained in the file prefs.js found in the
296  // profile directory.
297  // It is the "browser.search.selectedEngine" property.
298  if (search_engines.empty())
299    return -1;
300
301  std::string default_se_name =
302      ReadPrefsJsValue(profile_path, "browser.search.selectedEngine");
303
304  if (default_se_name.empty()) {
305    // browser.search.selectedEngine does not exist if the user has not changed
306    // from the default (or has selected the default).
307    // TODO: should fallback to 'browser.search.defaultengine' if selectedEngine
308    // is empty.
309    return -1;
310  }
311
312  int default_se_index = -1;
313  for (std::vector<TemplateURL*>::const_iterator iter = search_engines.begin();
314       iter != search_engines.end(); ++iter) {
315    if (default_se_name == UTF16ToUTF8((*iter)->short_name())) {
316      default_se_index = static_cast<int>(iter - search_engines.begin());
317      break;
318    }
319  }
320  if (default_se_index == -1) {
321    LOG(WARNING) <<
322        "Firefox default search engine not found in search engine list";
323  }
324
325  return default_se_index;
326}
327
328GURL GetHomepage(const FilePath& profile_path) {
329  std::string home_page_list =
330      ReadPrefsJsValue(profile_path, "browser.startup.homepage");
331
332  size_t seperator = home_page_list.find_first_of('|');
333  if (seperator == std::string::npos)
334    return GURL(home_page_list);
335
336  return GURL(home_page_list.substr(0, seperator));
337}
338
339bool IsDefaultHomepage(const GURL& homepage, const FilePath& app_path) {
340  if (!homepage.is_valid())
341    return false;
342
343  std::string default_homepages =
344      ReadBrowserConfigProp(app_path, "browser.startup.homepage");
345
346  size_t seperator = default_homepages.find_first_of('|');
347  if (seperator == std::string::npos)
348    return homepage.spec() == GURL(default_homepages).spec();
349
350  // Crack the string into separate homepage urls.
351  std::vector<std::string> urls;
352  base::SplitString(default_homepages, '|', &urls);
353
354  for (size_t i = 0; i < urls.size(); ++i) {
355    if (homepage.spec() == GURL(urls[i]).spec())
356      return true;
357  }
358
359  return false;
360}
361
362bool ParsePrefFile(const FilePath& pref_file, DictionaryValue* prefs) {
363  // The string that is before a pref key.
364  const std::string kUserPrefString = "user_pref(\"";
365  std::string contents;
366  if (!file_util::ReadFileToString(pref_file, &contents))
367    return false;
368
369  std::vector<std::string> lines;
370  Tokenize(contents, "\n", &lines);
371
372  for (std::vector<std::string>::const_iterator iter = lines.begin();
373       iter != lines.end(); ++iter) {
374    const std::string& line = *iter;
375    size_t start_key = line.find(kUserPrefString);
376    if (start_key == std::string::npos)
377      continue;  // Could be a comment or a blank line.
378    start_key += kUserPrefString.length();
379    size_t stop_key = line.find('"', start_key);
380    if (stop_key == std::string::npos) {
381      LOG(ERROR) << "Invalid key found in Firefox pref file '" <<
382          pref_file.value() << "' line is '" << line << "'.";
383      continue;
384    }
385    std::string key = line.substr(start_key, stop_key - start_key);
386    size_t start_value = line.find(',', stop_key + 1);
387    if (start_value == std::string::npos) {
388      LOG(ERROR) << "Invalid value found in Firefox pref file '" <<
389          pref_file.value() << "' line is '" << line << "'.";
390      continue;
391    }
392    size_t stop_value = line.find(");", start_value + 1);
393    if (stop_value == std::string::npos) {
394      LOG(ERROR) << "Invalid value found in Firefox pref file '" <<
395          pref_file.value() << "' line is '" << line << "'.";
396      continue;
397    }
398    std::string value = line.substr(start_value + 1,
399                                    stop_value - start_value - 1);
400    TrimWhitespace(value, TRIM_ALL, &value);
401    // Value could be a boolean.
402    bool is_value_true = LowerCaseEqualsASCII(value, "true");
403    if (is_value_true || LowerCaseEqualsASCII(value, "false")) {
404      prefs->SetBoolean(key, is_value_true);
405      continue;
406    }
407
408    // Value could be a string.
409    if (value.size() >= 2U &&
410        value[0] == '"' && value[value.size() - 1] == '"') {
411      value = value.substr(1, value.size() - 2);
412      // ValueString only accept valid UTF-8.  Simply ignore that entry if it is
413      // not UTF-8.
414      if (IsStringUTF8(value))
415        prefs->SetString(key, value);
416      else
417        VLOG(1) << "Non UTF8 value for key " << key << ", ignored.";
418      continue;
419    }
420
421    // Or value could be an integer.
422    int int_value = 0;
423    if (base::StringToInt(value, &int_value)) {
424      prefs->SetInteger(key, int_value);
425      continue;
426    }
427
428    LOG(ERROR) << "Invalid value found in Firefox pref file '"
429               << pref_file.value() << "' value is '" << value << "'.";
430  }
431  return true;
432}
433
434std::string GetPrefsJsValue(const std::string& content,
435                            const std::string& pref_key) {
436  // This file has the syntax: user_pref("key", value);
437  std::string search_for = std::string("user_pref(\"") + pref_key +
438                           std::string("\", ");
439  size_t prop_index = content.find(search_for);
440  if (prop_index == std::string::npos)
441    return std::string();
442
443  size_t start = prop_index + search_for.length();
444  size_t stop = std::string::npos;
445  if (start != std::string::npos) {
446    // Stop at the last ')' on this line.
447    stop = content.find("\n", start + 1);
448    stop = content.rfind(")", stop);
449  }
450
451  if (start == std::string::npos || stop == std::string::npos ||
452      stop < start) {
453    LOG(WARNING) << "Firefox property " << pref_key << " could not be parsed.";
454    return "";
455  }
456
457  // String values have double quotes we don't need to return to the caller.
458  if (content[start] == '\"' && content[stop - 1] == '\"') {
459    ++start;
460    --stop;
461  }
462
463  return content.substr(start, stop - start);
464}
465