extension_file_util.cc revision 21d179b334e59e9a3bfcaed4c4430bef1bc5759d
1// Copyright (c) 2010 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/extensions/extension_file_util.h"
6
7#include <map>
8#include <vector>
9
10#include "app/l10n_util.h"
11#include "base/file_util.h"
12#include "base/logging.h"
13#include "base/scoped_temp_dir.h"
14#include "base/utf_string_conversions.h"
15#include "chrome/common/extensions/extension.h"
16#include "chrome/common/extensions/extension_action.h"
17#include "chrome/common/extensions/extension_l10n_util.h"
18#include "chrome/common/extensions/extension_constants.h"
19#include "chrome/common/extensions/extension_resource.h"
20#include "chrome/common/json_value_serializer.h"
21#include "grit/generated_resources.h"
22#include "net/base/escape.h"
23#include "net/base/file_stream.h"
24
25namespace errors = extension_manifest_errors;
26
27namespace extension_file_util {
28
29// Validates locale info. Doesn't check if messages.json files are valid.
30static bool ValidateLocaleInfo(const Extension& extension, std::string* error);
31
32// Returns false and sets the error if script file can't be loaded,
33// or if it's not UTF-8 encoded.
34static bool IsScriptValid(const FilePath& path, const FilePath& relative_path,
35                          int message_id, std::string* error);
36
37const char kInstallDirectoryName[] = "Extensions";
38
39FilePath InstallExtension(const FilePath& unpacked_source_dir,
40                          const std::string& id,
41                          const std::string& version,
42                          const FilePath& all_extensions_dir) {
43  FilePath extension_dir = all_extensions_dir.AppendASCII(id);
44  FilePath version_dir;
45
46  // Create the extension directory if it doesn't exist already.
47  if (!file_util::PathExists(extension_dir)) {
48    if (!file_util::CreateDirectory(extension_dir))
49      return FilePath();
50  }
51
52  // Try to find a free directory. There can be legitimate conflicts in the case
53  // of overinstallation of the same version.
54  const int kMaxAttempts = 100;
55  for (int i = 0; i < kMaxAttempts; ++i) {
56    FilePath candidate = extension_dir.AppendASCII(
57        base::StringPrintf("%s_%u", version.c_str(), i));
58    if (!file_util::PathExists(candidate)) {
59      version_dir = candidate;
60      break;
61    }
62  }
63
64  if (version_dir.empty()) {
65    LOG(ERROR) << "Could not find a home for extension " << id << " with "
66               << "version " << version << ".";
67    return FilePath();
68  }
69
70  if (!file_util::Move(unpacked_source_dir, version_dir))
71    return FilePath();
72
73  return version_dir;
74}
75
76void UninstallExtension(const FilePath& extensions_dir,
77                        const std::string& id) {
78  // We don't care about the return value. If this fails (and it can, due to
79  // plugins that aren't unloaded yet, it will get cleaned up by
80  // ExtensionService::GarbageCollectExtensions).
81  file_util::Delete(extensions_dir.AppendASCII(id), true);  // recursive.
82}
83
84scoped_refptr<Extension> LoadExtension(const FilePath& extension_path,
85                                       Extension::Location location,
86                                       bool require_key,
87                                       std::string* error) {
88  FilePath manifest_path =
89      extension_path.Append(Extension::kManifestFilename);
90  if (!file_util::PathExists(manifest_path)) {
91    *error = l10n_util::GetStringUTF8(IDS_EXTENSION_MANIFEST_UNREADABLE);
92    return NULL;
93  }
94
95  JSONFileValueSerializer serializer(manifest_path);
96  scoped_ptr<Value> root(serializer.Deserialize(NULL, error));
97  if (!root.get()) {
98    if (error->empty()) {
99      // If |error| is empty, than the file could not be read.
100      // It would be cleaner to have the JSON reader give a specific error
101      // in this case, but other code tests for a file error with
102      // error->empty().  For now, be consistent.
103      *error = l10n_util::GetStringUTF8(IDS_EXTENSION_MANIFEST_UNREADABLE);
104    } else {
105      *error = base::StringPrintf("%s  %s",
106                                  errors::kManifestParseError,
107                                  error->c_str());
108    }
109    return NULL;
110  }
111
112  if (!root->IsType(Value::TYPE_DICTIONARY)) {
113    *error = l10n_util::GetStringUTF8(IDS_EXTENSION_MANIFEST_INVALID);
114    return NULL;
115  }
116
117  DictionaryValue* manifest = static_cast<DictionaryValue*>(root.get());
118  if (!extension_l10n_util::LocalizeExtension(extension_path, manifest, error))
119    return NULL;
120
121  scoped_refptr<Extension> extension(Extension::Create(
122      extension_path, location, *manifest, require_key, error));
123  if (!extension.get())
124    return NULL;
125
126  if (!ValidateExtension(extension.get(), error))
127    return NULL;
128
129  return extension;
130}
131
132bool ValidateExtension(Extension* extension, std::string* error) {
133  // Validate icons exist.
134  for (ExtensionIconSet::IconMap::const_iterator iter =
135           extension->icons().map().begin();
136       iter != extension->icons().map().end();
137       ++iter) {
138    const FilePath path = extension->GetResource(iter->second).GetFilePath();
139    if (!file_util::PathExists(path)) {
140      *error =
141          l10n_util::GetStringFUTF8(IDS_EXTENSION_LOAD_ICON_FAILED,
142                                    UTF8ToUTF16(iter->second));
143      return false;
144    }
145  }
146
147  // Theme resource validation.
148  if (extension->is_theme()) {
149    DictionaryValue* images_value = extension->GetThemeImages();
150    if (images_value) {
151      for (DictionaryValue::key_iterator iter = images_value->begin_keys();
152           iter != images_value->end_keys(); ++iter) {
153        std::string val;
154        if (images_value->GetStringWithoutPathExpansion(*iter, &val)) {
155          FilePath image_path = extension->path().AppendASCII(val);
156          if (!file_util::PathExists(image_path)) {
157            *error =
158                l10n_util::GetStringFUTF8(IDS_EXTENSION_INVALID_IMAGE_PATH,
159                    WideToUTF16(image_path.ToWStringHack()));
160            return false;
161          }
162        }
163      }
164    }
165
166    // Themes cannot contain other extension types.
167    return true;
168  }
169
170  // Validate that claimed script resources actually exist,
171  // and are UTF-8 encoded.
172  for (size_t i = 0; i < extension->content_scripts().size(); ++i) {
173    const UserScript& script = extension->content_scripts()[i];
174
175    for (size_t j = 0; j < script.js_scripts().size(); j++) {
176      const UserScript::File& js_script = script.js_scripts()[j];
177      const FilePath& path = ExtensionResource::GetFilePath(
178          js_script.extension_root(), js_script.relative_path());
179      if (!IsScriptValid(path, js_script.relative_path(),
180                         IDS_EXTENSION_LOAD_JAVASCRIPT_FAILED, error))
181        return false;
182    }
183
184    for (size_t j = 0; j < script.css_scripts().size(); j++) {
185      const UserScript::File& css_script = script.css_scripts()[j];
186      const FilePath& path = ExtensionResource::GetFilePath(
187          css_script.extension_root(), css_script.relative_path());
188      if (!IsScriptValid(path, css_script.relative_path(),
189                         IDS_EXTENSION_LOAD_CSS_FAILED, error))
190        return false;
191    }
192  }
193
194  // Validate claimed plugin paths.
195  for (size_t i = 0; i < extension->plugins().size(); ++i) {
196    const Extension::PluginInfo& plugin = extension->plugins()[i];
197    if (!file_util::PathExists(plugin.path)) {
198      *error =
199          l10n_util::GetStringFUTF8(
200              IDS_EXTENSION_LOAD_PLUGIN_PATH_FAILED,
201              WideToUTF16(plugin.path.ToWStringHack()));
202      return false;
203    }
204  }
205
206  // Validate icon location for page actions.
207  ExtensionAction* page_action = extension->page_action();
208  if (page_action) {
209    std::vector<std::string> icon_paths(*page_action->icon_paths());
210    if (!page_action->default_icon_path().empty())
211      icon_paths.push_back(page_action->default_icon_path());
212    for (std::vector<std::string>::iterator iter = icon_paths.begin();
213         iter != icon_paths.end(); ++iter) {
214      if (!file_util::PathExists(extension->GetResource(*iter).GetFilePath())) {
215        *error =
216            l10n_util::GetStringFUTF8(
217                IDS_EXTENSION_LOAD_ICON_FOR_PAGE_ACTION_FAILED,
218                UTF8ToUTF16(*iter));
219        return false;
220      }
221    }
222  }
223
224  // Validate icon location for browser actions.
225  // Note: browser actions don't use the icon_paths().
226  ExtensionAction* browser_action = extension->browser_action();
227  if (browser_action) {
228    std::string path = browser_action->default_icon_path();
229    if (!path.empty() &&
230        !file_util::PathExists(extension->GetResource(path).GetFilePath())) {
231        *error =
232            l10n_util::GetStringFUTF8(
233                IDS_EXTENSION_LOAD_ICON_FOR_BROWSER_ACTION_FAILED,
234                UTF8ToUTF16(path));
235        return false;
236    }
237  }
238
239  // Validate background page location.
240  if (!extension->background_url().is_empty()) {
241    FilePath page_path = ExtensionURLToRelativeFilePath(
242        extension->background_url());
243    const FilePath path = extension->GetResource(page_path).GetFilePath();
244    if (path.empty() || !file_util::PathExists(path)) {
245      *error =
246          l10n_util::GetStringFUTF8(
247              IDS_EXTENSION_LOAD_BACKGROUND_PAGE_FAILED,
248              WideToUTF16(page_path.ToWStringHack()));
249      return false;
250    }
251  }
252
253  // Validate path to the options page.  Don't check the URL for hosted apps,
254  // because they are expected to refer to an external URL.
255  if (!extension->options_url().is_empty() && !extension->is_hosted_app()) {
256    const FilePath options_path = ExtensionURLToRelativeFilePath(
257        extension->options_url());
258    const FilePath path = extension->GetResource(options_path).GetFilePath();
259    if (path.empty() || !file_util::PathExists(path)) {
260      *error =
261          l10n_util::GetStringFUTF8(
262              IDS_EXTENSION_LOAD_OPTIONS_PAGE_FAILED,
263              WideToUTF16(options_path.ToWStringHack()));
264      return false;
265    }
266  }
267
268  // Validate locale info.
269  if (!ValidateLocaleInfo(*extension, error))
270    return false;
271
272  // Check children of extension root to see if any of them start with _ and is
273  // not on the reserved list.
274  if (!CheckForIllegalFilenames(extension->path(), error)) {
275    return false;
276  }
277
278  return true;
279}
280
281void GarbageCollectExtensions(
282    const FilePath& install_directory,
283    const std::map<std::string, FilePath>& extension_paths) {
284  // Nothing to clean up if it doesn't exist.
285  if (!file_util::DirectoryExists(install_directory))
286    return;
287
288  VLOG(1) << "Garbage collecting extensions...";
289  file_util::FileEnumerator enumerator(install_directory,
290                                       false,  // Not recursive.
291                                       file_util::FileEnumerator::DIRECTORIES);
292  FilePath extension_path;
293  for (extension_path = enumerator.Next(); !extension_path.value().empty();
294       extension_path = enumerator.Next()) {
295    std::string extension_id = WideToASCII(
296        extension_path.BaseName().ToWStringHack());
297
298    // Delete directories that aren't valid IDs.
299    if (!Extension::IdIsValid(extension_id)) {
300      LOG(WARNING) << "Invalid extension ID encountered in extensions "
301                      "directory: " << extension_id;
302      VLOG(1) << "Deleting invalid extension directory "
303              << WideToASCII(extension_path.ToWStringHack()) << ".";
304      file_util::Delete(extension_path, true);  // Recursive.
305      continue;
306    }
307
308    std::map<std::string, FilePath>::const_iterator iter =
309        extension_paths.find(extension_id);
310
311    // If there is no entry in the prefs file, just delete the directory and
312    // move on. This can legitimately happen when an uninstall does not
313    // complete, for example, when a plugin is in use at uninstall time.
314    if (iter == extension_paths.end()) {
315      VLOG(1) << "Deleting unreferenced install for directory "
316              << WideToASCII(extension_path.ToWStringHack()) << ".";
317      file_util::Delete(extension_path, true);  // Recursive.
318      continue;
319    }
320
321    // Clean up old version directories.
322    file_util::FileEnumerator versions_enumerator(
323        extension_path,
324        false,  // Not recursive.
325        file_util::FileEnumerator::DIRECTORIES);
326    for (FilePath version_dir = versions_enumerator.Next();
327         !version_dir.value().empty();
328         version_dir = versions_enumerator.Next()) {
329      if (version_dir.BaseName() != iter->second.BaseName()) {
330        VLOG(1) << "Deleting old version for directory "
331                << WideToASCII(version_dir.ToWStringHack()) << ".";
332        file_util::Delete(version_dir, true);  // Recursive.
333      }
334    }
335  }
336}
337
338ExtensionMessageBundle* LoadExtensionMessageBundle(
339    const FilePath& extension_path,
340    const std::string& default_locale,
341    std::string* error) {
342  error->clear();
343  // Load locale information if available.
344  FilePath locale_path = extension_path.Append(Extension::kLocaleFolder);
345  if (!file_util::PathExists(locale_path))
346    return NULL;
347
348  std::set<std::string> locales;
349  if (!extension_l10n_util::GetValidLocales(locale_path, &locales, error))
350    return NULL;
351
352  if (default_locale.empty() ||
353      locales.find(default_locale) == locales.end()) {
354    *error = l10n_util::GetStringUTF8(
355        IDS_EXTENSION_LOCALES_NO_DEFAULT_LOCALE_SPECIFIED);
356    return NULL;
357  }
358
359  ExtensionMessageBundle* message_bundle =
360      extension_l10n_util::LoadMessageCatalogs(
361          locale_path,
362          default_locale,
363          extension_l10n_util::CurrentLocaleOrDefault(),
364          locales,
365          error);
366
367  return message_bundle;
368}
369
370static bool ValidateLocaleInfo(const Extension& extension, std::string* error) {
371  // default_locale and _locales have to be both present or both missing.
372  const FilePath path = extension.path().Append(Extension::kLocaleFolder);
373  bool path_exists = file_util::PathExists(path);
374  std::string default_locale = extension.default_locale();
375
376  // If both default locale and _locales folder are empty, skip verification.
377  if (default_locale.empty() && !path_exists)
378    return true;
379
380  if (default_locale.empty() && path_exists) {
381    *error = l10n_util::GetStringUTF8(
382        IDS_EXTENSION_LOCALES_NO_DEFAULT_LOCALE_SPECIFIED);
383    return false;
384  } else if (!default_locale.empty() && !path_exists) {
385    *error = errors::kLocalesTreeMissing;
386    return false;
387  }
388
389  // Treat all folders under _locales as valid locales.
390  file_util::FileEnumerator locales(path,
391                                    false,
392                                    file_util::FileEnumerator::DIRECTORIES);
393
394  std::set<std::string> all_locales;
395  extension_l10n_util::GetAllLocales(&all_locales);
396  const FilePath default_locale_path = path.AppendASCII(default_locale);
397  bool has_default_locale_message_file = false;
398
399  FilePath locale_path;
400  while (!(locale_path = locales.Next()).empty()) {
401    if (extension_l10n_util::ShouldSkipValidation(path, locale_path,
402                                                  all_locales))
403      continue;
404
405    FilePath messages_path =
406        locale_path.Append(Extension::kMessagesFilename);
407
408    if (!file_util::PathExists(messages_path)) {
409      *error = base::StringPrintf(
410          "%s %s", errors::kLocalesMessagesFileMissing,
411          WideToUTF8(messages_path.ToWStringHack()).c_str());
412      return false;
413    }
414
415    if (locale_path == default_locale_path)
416      has_default_locale_message_file = true;
417  }
418
419  // Only message file for default locale has to exist.
420  if (!has_default_locale_message_file) {
421    *error = errors::kLocalesNoDefaultMessages;
422    return false;
423  }
424
425  return true;
426}
427
428static bool IsScriptValid(const FilePath& path,
429                          const FilePath& relative_path,
430                          int message_id,
431                          std::string* error) {
432  std::string content;
433  if (!file_util::PathExists(path) ||
434      !file_util::ReadFileToString(path, &content)) {
435    *error = l10n_util::GetStringFUTF8(
436        message_id,
437        WideToUTF16(relative_path.ToWStringHack()));
438    return false;
439  }
440
441  if (!IsStringUTF8(content)) {
442    *error = l10n_util::GetStringFUTF8(
443        IDS_EXTENSION_BAD_FILE_ENCODING,
444        WideToUTF16(relative_path.ToWStringHack()));
445    return false;
446  }
447
448  return true;
449}
450
451bool CheckForIllegalFilenames(const FilePath& extension_path,
452                              std::string* error) {
453  // Reserved underscore names.
454  static const FilePath::CharType* reserved_names[] = {
455    Extension::kLocaleFolder,
456    FILE_PATH_LITERAL("__MACOSX"),
457  };
458  static std::set<FilePath::StringType> reserved_underscore_names(
459      reserved_names, reserved_names + arraysize(reserved_names));
460
461  // Enumerate all files and directories in the extension root.
462  // There is a problem when using pattern "_*" with FileEnumerator, so we have
463  // to cheat with find_first_of and match all.
464  file_util::FileEnumerator all_files(
465    extension_path,
466    false,
467    static_cast<file_util::FileEnumerator::FILE_TYPE>(
468        file_util::FileEnumerator::DIRECTORIES |
469          file_util::FileEnumerator::FILES));
470
471  FilePath file;
472  while (!(file = all_files.Next()).empty()) {
473    FilePath::StringType filename = file.BaseName().value();
474    // Skip all that don't start with "_".
475    if (filename.find_first_of(FILE_PATH_LITERAL("_")) != 0) continue;
476    if (reserved_underscore_names.find(filename) ==
477        reserved_underscore_names.end()) {
478      *error = base::StringPrintf(
479          "Cannot load extension with file or directory name %s. "
480          "Filenames starting with \"_\" are reserved for use by the system.",
481          filename.c_str());
482      return false;
483    }
484  }
485
486  return true;
487}
488
489FilePath ExtensionURLToRelativeFilePath(const GURL& url) {
490  std::string url_path = url.path();
491  if (url_path.empty() || url_path[0] != '/')
492    return FilePath();
493
494  // Drop the leading slashes and convert %-encoded UTF8 to regular UTF8.
495  std::string file_path = UnescapeURLComponent(url_path,
496      UnescapeRule::SPACES | UnescapeRule::URL_SPECIAL_CHARS);
497  size_t skip = file_path.find_first_not_of("/\\");
498  if (skip != file_path.npos)
499    file_path = file_path.substr(skip);
500
501  FilePath path =
502#if defined(OS_POSIX)
503    FilePath(file_path);
504#elif defined(OS_WIN)
505    FilePath(UTF8ToWide(file_path));
506#else
507    FilePath();
508    NOTIMPLEMENTED();
509#endif
510
511  // It's still possible for someone to construct an annoying URL whose path
512  // would still wind up not being considered relative at this point.
513  // For example: chrome-extension://id/c:////foo.html
514  if (path.IsAbsolute())
515    return FilePath();
516
517  return path;
518}
519
520}  // namespace extension_file_util
521