Allow extensions to be packed where the resources are symlinks.

BUG=123488
TEST=unit_tests


Review URL: http://codereview.chromium.org/10041007

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@133270 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/common/extensions/extension.cc b/chrome/common/extensions/extension.cc
index 8dab806..03230820 100644
--- a/chrome/common/extensions/extension.cc
+++ b/chrome/common/extensions/extension.cc
@@ -2895,12 +2895,20 @@
 #elif defined(OS_WIN)
   FilePath relative_file_path(UTF8ToWide(new_path));
 #endif
-  return ExtensionResource(id(), path(), relative_file_path);
+  ExtensionResource r(id(), path(), relative_file_path);
+  if ((creation_flags() & Extension::FOLLOW_SYMLINKS_ANYWHERE)) {
+    r.set_follow_symlinks_anywhere();
+  }
+  return r;
 }
 
 ExtensionResource Extension::GetResource(
     const FilePath& relative_file_path) const {
-  return ExtensionResource(id(), path(), relative_file_path);
+  ExtensionResource r(id(), path(), relative_file_path);
+  if ((creation_flags() & Extension::FOLLOW_SYMLINKS_ANYWHERE)) {
+    r.set_follow_symlinks_anywhere();
+  }
+  return r;
 }
 
 // TODO(rafaelw): Move ParsePEMKeyBytes, ProducePEM & FormatPEMForOutput to a
diff --git a/chrome/common/extensions/extension.h b/chrome/common/extensions/extension.h
index 29ddca6d..749cee9 100644
--- a/chrome/common/extensions/extension.h
+++ b/chrome/common/extensions/extension.h
@@ -245,6 +245,11 @@
     // |FROM_BOOKMARK| indicates the extension was created using a mock App
     // created from a bookmark.
     FROM_BOOKMARK = 1 << 5,
+
+    // |FOLLOW_SYMLINKS_ANYWHERE| means that resources can be symlinks to
+    // anywhere in the filesystem, rather than being restricted to the
+    // extension directory.
+    FOLLOW_SYMLINKS_ANYWHERE = 1 << 6,
   };
 
   static scoped_refptr<Extension> Create(const FilePath& path,
diff --git a/chrome/common/extensions/extension_file_util.cc b/chrome/common/extensions/extension_file_util.cc
index a7a5cc9..3f9a9b9 100644
--- a/chrome/common/extensions/extension_file_util.cc
+++ b/chrome/common/extensions/extension_file_util.cc
@@ -19,8 +19,8 @@
 #include "chrome/common/chrome_paths.h"
 #include "chrome/common/extensions/extension.h"
 #include "chrome/common/extensions/extension_action.h"
-#include "chrome/common/extensions/extension_manifest_constants.h"
 #include "chrome/common/extensions/extension_l10n_util.h"
+#include "chrome/common/extensions/extension_manifest_constants.h"
 #include "chrome/common/extensions/extension_message_bundle.h"
 #include "chrome/common/extensions/extension_messages.h"
 #include "chrome/common/extensions/extension_resource.h"
@@ -231,13 +231,22 @@
 
   // Validate that claimed script resources actually exist,
   // and are UTF-8 encoded.
+  ExtensionResource::SymlinkPolicy symlink_policy;
+  if ((extension->creation_flags() &
+       Extension::FOLLOW_SYMLINKS_ANYWHERE) != 0) {
+    symlink_policy = ExtensionResource::FOLLOW_SYMLINKS_ANYWHERE;
+  } else {
+    symlink_policy = ExtensionResource::SYMLINKS_MUST_RESOLVE_WITHIN_ROOT;
+  }
+
   for (size_t i = 0; i < extension->content_scripts().size(); ++i) {
     const UserScript& script = extension->content_scripts()[i];
 
     for (size_t j = 0; j < script.js_scripts().size(); j++) {
       const UserScript::File& js_script = script.js_scripts()[j];
       const FilePath& path = ExtensionResource::GetFilePath(
-          js_script.extension_root(), js_script.relative_path());
+          js_script.extension_root(), js_script.relative_path(),
+          symlink_policy);
       if (!IsScriptValid(path, js_script.relative_path(),
                          IDS_EXTENSION_LOAD_JAVASCRIPT_FAILED, error))
         return false;
@@ -246,7 +255,8 @@
     for (size_t j = 0; j < script.css_scripts().size(); j++) {
       const UserScript::File& css_script = script.css_scripts()[j];
       const FilePath& path = ExtensionResource::GetFilePath(
-          css_script.extension_root(), css_script.relative_path());
+          css_script.extension_root(), css_script.relative_path(),
+          symlink_policy);
       if (!IsScriptValid(path, css_script.relative_path(),
                          IDS_EXTENSION_LOAD_CSS_FAILED, error))
         return false;
diff --git a/chrome/common/extensions/extension_resource.cc b/chrome/common/extensions/extension_resource.cc
index 1b344c93..6642c89 100644
--- a/chrome/common/extensions/extension_resource.cc
+++ b/chrome/common/extensions/extension_resource.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -16,11 +16,16 @@
                                      const FilePath& relative_path)
     : extension_id_(extension_id),
       extension_root_(extension_root),
-      relative_path_(relative_path) {
+      relative_path_(relative_path),
+      follow_symlinks_anywhere_(false) {
 }
 
 ExtensionResource::~ExtensionResource() {}
 
+void ExtensionResource::set_follow_symlinks_anywhere() {
+  follow_symlinks_anywhere_ = true;
+}
+
 const FilePath& ExtensionResource::GetFilePath() const {
   if (extension_root_.empty() || relative_path_.empty()) {
     DCHECK(full_resource_path_.empty());
@@ -31,14 +36,18 @@
   if (!full_resource_path_.empty())
     return full_resource_path_;
 
-  full_resource_path_ =
-      GetFilePath(extension_root_, relative_path_);
+  full_resource_path_ = GetFilePath(
+      extension_root_, relative_path_,
+      follow_symlinks_anywhere_ ?
+          FOLLOW_SYMLINKS_ANYWHERE : SYMLINKS_MUST_RESOLVE_WITHIN_ROOT);
   return full_resource_path_;
 }
 
 // static
 FilePath ExtensionResource::GetFilePath(
-    const FilePath& extension_root, const FilePath& relative_path) {
+    const FilePath& extension_root,
+    const FilePath& relative_path,
+    SymlinkPolicy symlink_policy) {
   // We need to resolve the parent references in the extension_root
   // path on its own because IsParent doesn't like parent references.
   FilePath clean_extension_root(extension_root);
@@ -47,6 +56,26 @@
 
   FilePath full_path = clean_extension_root.Append(relative_path);
 
+  // If we are allowing the file to be a symlink outside of the root, then the
+  // path before resolving the symlink must still be within it.
+  if (symlink_policy == FOLLOW_SYMLINKS_ANYWHERE) {
+    std::vector<FilePath::StringType> components;
+    relative_path.GetComponents(&components);
+    int depth = 0;
+
+    for (std::vector<FilePath::StringType>::const_iterator
+         i = components.begin(); i != components.end(); i++) {
+      if (*i == FilePath::kParentDirectory) {
+        depth--;
+      } else {
+        depth++;
+      }
+      if (depth < 0) {
+        return FilePath();
+      }
+    }
+  }
+
   // We must resolve the absolute path of the combined path when
   // the relative path contains references to a parent folder (i.e., '..').
   // We also check if the path exists because the posix version of AbsolutePath
@@ -56,7 +85,8 @@
   // TODO(mad): Fix this once AbsolutePath is unified.
   if (file_util::AbsolutePath(&full_path) &&
       file_util::PathExists(full_path) &&
-      clean_extension_root.IsParent(full_path)) {
+      (symlink_policy == FOLLOW_SYMLINKS_ANYWHERE ||
+       clean_extension_root.IsParent(full_path))) {
     return full_path;
   }
 
diff --git a/chrome/common/extensions/extension_resource.h b/chrome/common/extensions/extension_resource.h
index 4a7d8cf..47726ce 100644
--- a/chrome/common/extensions/extension_resource.h
+++ b/chrome/common/extensions/extension_resource.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -16,6 +16,13 @@
 // depending on locale.
 class ExtensionResource {
  public:
+  // SymlinkPolicy decides whether we'll allow resources to be a symlink to
+  // anywhere, or whether they must end up within the extension root.
+  enum SymlinkPolicy {
+    SYMLINKS_MUST_RESOLVE_WITHIN_ROOT,
+    FOLLOW_SYMLINKS_ANYWHERE,
+  };
+
   ExtensionResource();
 
   ExtensionResource(const std::string& extension_id,
@@ -24,6 +31,11 @@
 
   ~ExtensionResource();
 
+  // set_follow_symlinks_anywhere allows the resource to be a symlink to
+  // anywhere in the filesystem. By default, resources have to be within
+  // |extension_root| after resolving symlinks.
+  void set_follow_symlinks_anywhere();
+
   // Returns actual path to the resource (default or locale specific). In the
   // browser process, this will DCHECK if not called on the file thread. To
   // easily load extension images on the UI thread, see ImageLoadingTracker.
@@ -33,8 +45,13 @@
   // localization. In the browser process, this will DCHECK if not called on the
   // file thread. To easily load extension images on the UI thread, see
   // ImageLoadingTracker.
+  //
+  // The relative path must not resolve to a location outside of
+  // |extension_root|. Iff |file_can_symlink_outside_root| is true, then the
+  // file can be a symlink that links outside of |extension_root|.
   static FilePath GetFilePath(const FilePath& extension_root,
-                              const FilePath& relative_path);
+                              const FilePath& relative_path,
+                              SymlinkPolicy symlink_policy);
 
   // Getters
   const std::string& extension_id() const { return extension_id_; }
@@ -58,6 +75,10 @@
   // Relative path to resource.
   FilePath relative_path_;
 
+  // If |follow_symlinks_anywhere_| is true then the resource itself must be
+  // within |extension_root|, but it can be a symlink to a file that is not.
+  bool follow_symlinks_anywhere_;
+
   // Full path to extension resource. Starts empty.
   mutable FilePath full_resource_path_;
 };
diff --git a/chrome/common/extensions/extension_resource_unittest.cc b/chrome/common/extensions/extension_resource_unittest.cc
index f87314a..9b3e2d0 100644
--- a/chrome/common/extensions/extension_resource_unittest.cc
+++ b/chrome/common/extensions/extension_resource_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -43,6 +43,64 @@
   EXPECT_TRUE(resource.GetFilePath().empty());
 }
 
+TEST(ExtensionResourceTest, ResourcesOutsideOfPath) {
+  ScopedTempDir temp;
+  ASSERT_TRUE(temp.CreateUniqueTempDir());
+
+  FilePath inner_dir = temp.path().AppendASCII("directory");
+  ASSERT_TRUE(file_util::CreateDirectory(inner_dir));
+  FilePath inner_file = inner_dir.AppendASCII("inner");
+  FilePath outer_file = temp.path().AppendASCII("outer");
+  ASSERT_TRUE(file_util::WriteFile(outer_file, "X", 1));
+  ASSERT_TRUE(file_util::WriteFile(inner_file, "X", 1));
+  std::string extension_id = extension_test_util::MakeId("test");
+
+#if defined(OS_POSIX)
+  FilePath symlink_file = inner_dir.AppendASCII("symlink");
+  file_util::CreateSymbolicLink(
+      FilePath().AppendASCII("..").AppendASCII("outer"),
+      symlink_file);
+#endif
+
+  // A non-packing extension should be able to access the file within the
+  // directory.
+  ExtensionResource r1(extension_id, inner_dir,
+                       FilePath().AppendASCII("inner"));
+  EXPECT_FALSE(r1.GetFilePath().empty());
+
+  // ... but not a relative path that walks out of |inner_dir|.
+  ExtensionResource r2(extension_id, inner_dir,
+                       FilePath().AppendASCII("..").AppendASCII("outer"));
+  EXPECT_TRUE(r2.GetFilePath().empty());
+
+  // A packing extension should also be able to access the file within the
+  // directory.
+  ExtensionResource r3(extension_id, inner_dir,
+                       FilePath().AppendASCII("inner"));
+  r3.set_follow_symlinks_anywhere();
+  EXPECT_FALSE(r3.GetFilePath().empty());
+
+  // ... but, again, not a relative path that walks out of |inner_dir|.
+  ExtensionResource r4(extension_id, inner_dir,
+                       FilePath().AppendASCII("..").AppendASCII("outer"));
+  r4.set_follow_symlinks_anywhere();
+  EXPECT_TRUE(r4.GetFilePath().empty());
+
+#if defined(OS_POSIX)
+  // The non-packing extension should also not be able to access a resource that
+  // symlinks out of the directory.
+  ExtensionResource r5(extension_id, inner_dir,
+                       FilePath().AppendASCII("symlink"));
+  EXPECT_TRUE(r5.GetFilePath().empty());
+
+  // ... but a packing extension can.
+  ExtensionResource r6(extension_id, inner_dir,
+                       FilePath().AppendASCII("symlink"));
+  r6.set_follow_symlinks_anywhere();
+  EXPECT_FALSE(r6.GetFilePath().empty());
+#endif
+}
+
 // crbug.com/108721. Disabled on Windows due to crashing on Vista.
 #if defined(OS_WIN)
 #define CreateWithAllResourcesOnDisk DISABLED_CreateWithAllResourcesOnDisk