Skip to content

Commit

Permalink
Add first class support for user scripts.
Browse files Browse the repository at this point in the history
Original review: http://codereview.chromium.org/340057

TBR=mpcomplete@chromium.org
BUG=22103
TEST=Install a user script (such as from userscripts.org). You should get the extension install UI and the
script should show up in the extension management UI. It should also work, though some scripts use
Firefox-specific APIs and those won't work in Chromium.

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@30925 0039d316-1c4b-4281-b951-d872f2087c98
  • Loading branch information
aa@chromium.org committed Nov 4, 2009
1 parent 6c856ae commit 6657afa
Show file tree
Hide file tree
Showing 27 changed files with 676 additions and 101 deletions.
5 changes: 5 additions & 0 deletions base/string_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,11 @@ bool EndsWithT(const STR& str, const STR& search, bool case_sensitive) {
}
}

bool EndsWith(const std::string& str, const std::string& search,
bool case_sensitive) {
return EndsWithT(str, search, case_sensitive);
}

bool EndsWith(const std::wstring& str, const std::wstring& search,
bool case_sensitive) {
return EndsWithT(str, search, case_sensitive);
Expand Down
3 changes: 3 additions & 0 deletions base/string_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ bool StartsWith(const string16& str,
bool case_sensitive);

// Returns true if str ends with search, or false otherwise.
bool EndsWith(const std::string& str,
const std::string& search,
bool case_sensitive);
bool EndsWith(const std::wstring& str,
const std::wstring& search,
bool case_sensitive);
Expand Down
44 changes: 34 additions & 10 deletions chrome/browser/download/download_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/user_script.h"
#include "chrome/common/notification_service.h"
#include "chrome/common/notification_type.h"
#include "chrome/common/platform_util.h"
Expand Down Expand Up @@ -1243,14 +1244,25 @@ void DownloadManager::OpenChromeExtension(const FilePath& full_path,
nservice->Notify(NotificationType::EXTENSION_READY_FOR_INSTALL,
Source<DownloadManager>(this),
NotificationService::NoDetails());
CrxInstaller::Start(full_path,
service->install_directory(),
Extension::INTERNAL,
"", // no expected id
true, // please delete crx on completion
true, // privilege increase allowed
service,
new ExtensionInstallUI(profile_));
if (UserScript::HasUserScriptFileExtension(full_path)) {
CrxInstaller::InstallUserScript(
full_path,
download_url,
service->install_directory(),
true, // please delete crx on completion
service,
new ExtensionInstallUI(profile_));
} else {
CrxInstaller::Start(
full_path,
service->install_directory(),
Extension::INTERNAL,
"", // no expected id
true, // please delete crx on completion
true, // privilege increase allowed
service,
new ExtensionInstallUI(profile_));
}
}
}

Expand Down Expand Up @@ -1453,11 +1465,23 @@ void DownloadManager::GenerateSafeFilename(const std::string& mime_type,
}

bool DownloadManager::IsExtensionInstall(const DownloadItem* item) {
return item->mime_type() == Extension::kMimeType && !item->save_as();
if (item->save_as())
return false;

if (UserScript::HasUserScriptFileExtension(item->original_name()))
return true;

return item->mime_type() == Extension::kMimeType;
}

bool DownloadManager::IsExtensionInstall(const DownloadCreateInfo* info) {
return info->mime_type == Extension::kMimeType && !info->save_as;
if (info->save_as)
return false;

if (UserScript::HasUserScriptFileExtension(info->path))
return true;

return info->mime_type == Extension::kMimeType;
}

// Operations posted to us from the history service ----------------------------
Expand Down
138 changes: 138 additions & 0 deletions chrome/browser/extensions/convert_user_script.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright (c) 2009 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.

#include "chrome/browser/extensions/convert_user_script.h"

#include <string>
#include <vector>

#include "base/file_path.h"
#include "base/file_util.h"
#include "base/scoped_temp_dir.h"
#include "base/sha2.h"
#include "base/string_util.h"
#include "chrome/browser/extensions/user_script_master.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/extensions/user_script.h"
#include "chrome/common/json_value_serializer.h"
#include "googleurl/src/gurl.h"
#include "net/base/base64.h"

namespace keys = extension_manifest_keys;

Extension* ConvertUserScriptToExtension(const FilePath& user_script_path,
const GURL& original_url,
std::string* error){
std::string content;
if (!file_util::ReadFileToString(user_script_path, &content)) {
*error = "Could not read source file: " +
WideToASCII(user_script_path.ToWStringHack());
return NULL;
}

UserScript script;
if (!UserScriptMaster::ScriptReloader::ParseMetadataHeader(content,
&script)) {
*error = "Invalid script header.";
return NULL;
}

ScopedTempDir temp_dir;
if (!temp_dir.CreateUniqueTempDir()) {
*error = "Could not create temporary directory.";
return NULL;
}

// Create the manifest
scoped_ptr<DictionaryValue> root(new DictionaryValue);
std::string script_name;
if (!script.name().empty() && !script.name_space().empty())
script_name = script.name_space() + "/" + script.name();
else
script_name = original_url.spec();

// Create the public key.
// User scripts are not signed, but the public key for an extension doubles as
// its unique identity, and we need one of those. A user script's unique
// identity is its namespace+name, so we hash that to create a public key.
// There will be no corresponding private key, which means user scripts cannot
// be auto-updated, or claimed in the gallery.
char raw[base::SHA256_LENGTH] = {0};
std::string key;
base::SHA256HashString(script_name, raw, base::SHA256_LENGTH);
net::Base64Encode(std::string(raw, base::SHA256_LENGTH), &key);

// The script may not have a name field, but we need one for an extension. If
// it is missing, use the filename of the original URL.
if (!script.name().empty())
root->SetString(keys::kName, script.name());
else
root->SetString(keys::kName, original_url.ExtractFileName());

root->SetString(keys::kDescription, script.description());
root->SetString(keys::kVersion, "1.0");
root->SetString(keys::kPublicKey, key);
root->SetBoolean(keys::kConvertedFromUserScript, true);

ListValue* js_files = new ListValue();
js_files->Append(Value::CreateStringValue("script.js"));

// If the script provides its own match patterns, we use those. Otherwise, we
// generate some using the include globs.
ListValue* matches = new ListValue();
if (!script.url_patterns().empty()) {
for (size_t i = 0; i < script.url_patterns().size(); ++i) {
matches->Append(Value::CreateStringValue(
script.url_patterns()[i].GetAsString()));
}
} else {
// TODO(aa): Derive tighter matches where possible.
matches->Append(Value::CreateStringValue("http://*/*"));
matches->Append(Value::CreateStringValue("https://*/*"));
}

ListValue* includes = new ListValue();
for (size_t i = 0; i < script.globs().size(); ++i)
includes->Append(Value::CreateStringValue(script.globs().at(i)));

ListValue* excludes = new ListValue();
for (size_t i = 0; i < script.exclude_globs().size(); ++i)
excludes->Append(Value::CreateStringValue(script.exclude_globs().at(i)));

DictionaryValue* content_script = new DictionaryValue();
content_script->Set(keys::kMatches, matches);
content_script->Set(keys::kIncludeGlobs, includes);
content_script->Set(keys::kExcludeGlobs, excludes);
content_script->Set(keys::kJs, js_files);

ListValue* content_scripts = new ListValue();
content_scripts->Append(content_script);

root->Set(keys::kContentScripts, content_scripts);

FilePath manifest_path = temp_dir.path().AppendASCII(
Extension::kManifestFilename);
JSONFileValueSerializer serializer(manifest_path);
if (!serializer.Serialize(*root)) {
*error = "Could not write JSON.";
return NULL;
}

// Write the script file.
if (!file_util::CopyFile(user_script_path,
temp_dir.path().AppendASCII("script.js"))) {
*error = "Could not copy script file.";
return NULL;
}

scoped_ptr<Extension> extension(new Extension(temp_dir.path()));
if (!extension->InitFromValue(*root, false, error)) {
NOTREACHED() << "Could not init extension " << *error;
return NULL;
}

temp_dir.Take(); // The caller takes ownership of the directory.
return extension.release();
}
23 changes: 23 additions & 0 deletions chrome/browser/extensions/convert_user_script.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) 2009 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.

#ifndef CHROME_BROWSER_EXTENSIONS_CONVERT_USER_SCRIPT_H_
#define CHROME_BROWSER_EXTENSIONS_CONVERT_USER_SCRIPT_H_

#include <string>

class Extension;
class FilePath;
class GURL;

// Wraps the specified user script in an extension. The extension is created
// unpacked in the system temp dir. Returns a valid extension that the caller
// should take ownership on success, or NULL and |error| on failure.
//
// NOTE: This function does file IO and should not be called on the UI thread.
Extension* ConvertUserScriptToExtension(const FilePath& user_script,
const GURL& original_url,
std::string* error);

#endif // CHROME_BROWSER_EXTENSIONS_CONVERT_USER_SCRIPT_H_
88 changes: 88 additions & 0 deletions chrome/browser/extensions/convert_user_script_unittest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) 2009 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.

#include <string>
#include <vector>

#include "base/file_path.h"
#include "base/file_util.h"
#include "base/path_service.h"
#include "base/scoped_ptr.h"
#include "chrome/browser/extensions/convert_user_script.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/extensions/extension.h"
#include "testing/gtest/include/gtest/gtest.h"

TEST(ExtensionFromUserScript, Basic) {
FilePath test_file;
ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &test_file));
test_file = test_file.AppendASCII("extensions")
.AppendASCII("user_script_basic.user.js");

std::string error;
scoped_ptr<Extension> extension(ConvertUserScriptToExtension(
test_file, GURL("http://www.google.com/foo"), &error));

ASSERT_TRUE(extension.get());
EXPECT_EQ("", error);

// Validate generated extension metadata.
EXPECT_EQ("My user script", extension->name());
EXPECT_EQ("1.0", extension->VersionString());
EXPECT_EQ("Does totally awesome stuff.", extension->description());
EXPECT_EQ("IhCFCg9PMQTAcJdc9ytUP99WME+4yh6aMnM1uupkovo=",
extension->public_key());

ASSERT_EQ(1u, extension->content_scripts().size());
const UserScript& script = extension->content_scripts()[0];
ASSERT_EQ(2u, script.globs().size());
EXPECT_EQ("http://www.google.com/*", script.globs().at(0));
EXPECT_EQ("http://www.yahoo.com/*", script.globs().at(1));
ASSERT_EQ(1u, script.exclude_globs().size());
EXPECT_EQ("*foo*", script.exclude_globs().at(0));
ASSERT_EQ(1u, script.url_patterns().size());
EXPECT_EQ("http://www.google.com/*", script.url_patterns()[0].GetAsString());

// Make sure the files actually exist on disk.
EXPECT_TRUE(file_util::PathExists(
extension->path().Append(script.js_scripts()[0].relative_path())));
EXPECT_TRUE(file_util::PathExists(
extension->path().AppendASCII(Extension::kManifestFilename)));
}

TEST(ExtensionFromUserScript, NoMetdata) {
FilePath test_file;
ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &test_file));
test_file = test_file.AppendASCII("extensions")
.AppendASCII("user_script_no_metadata.user.js");

std::string error;
scoped_ptr<Extension> extension(ConvertUserScriptToExtension(
test_file, GURL("http://www.google.com/foo/bar.user.js?monkey"), &error));

ASSERT_TRUE(extension.get());
EXPECT_EQ("", error);

// Validate generated extension metadata.
EXPECT_EQ("bar.user.js", extension->name());
EXPECT_EQ("1.0", extension->VersionString());
EXPECT_EQ("", extension->description());
EXPECT_EQ("k1WxKx54hX6tfl5gQaXD/m4d9QUMwRdXWM4RW+QkWcY=",
extension->public_key());

ASSERT_EQ(1u, extension->content_scripts().size());
const UserScript& script = extension->content_scripts()[0];
ASSERT_EQ(1u, script.globs().size());
EXPECT_EQ("*", script.globs()[0]);
EXPECT_EQ(0u, script.exclude_globs().size());
ASSERT_EQ(2u, script.url_patterns().size());
EXPECT_EQ("http://*/*", script.url_patterns()[0].GetAsString());
EXPECT_EQ("https://*/*", script.url_patterns()[1].GetAsString());

// Make sure the files actually exist on disk.
EXPECT_TRUE(file_util::PathExists(
extension->path().Append(script.js_scripts()[0].relative_path())));
EXPECT_TRUE(file_util::PathExists(
extension->path().AppendASCII(Extension::kManifestFilename)));
}
Loading

0 comments on commit 6657afa

Please sign in to comment.