diff options
Diffstat (limited to 'chrome')
25 files changed, 668 insertions, 101 deletions
diff --git a/chrome/browser/download/download_manager.cc b/chrome/browser/download/download_manager.cc index eef8b04..bb0a280 100644 --- a/chrome/browser/download/download_manager.cc +++ b/chrome/browser/download/download_manager.cc @@ -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" @@ -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_)); + } } } @@ -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 ---------------------------- diff --git a/chrome/browser/extensions/convert_user_script.cc b/chrome/browser/extensions/convert_user_script.cc new file mode 100644 index 0000000..70de6d9 --- /dev/null +++ b/chrome/browser/extensions/convert_user_script.cc @@ -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(); +} diff --git a/chrome/browser/extensions/convert_user_script.h b/chrome/browser/extensions/convert_user_script.h new file mode 100644 index 0000000..ebc9ee2 --- /dev/null +++ b/chrome/browser/extensions/convert_user_script.h @@ -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_ diff --git a/chrome/browser/extensions/convert_user_script_unittest.cc b/chrome/browser/extensions/convert_user_script_unittest.cc new file mode 100644 index 0000000..b7aea54 --- /dev/null +++ b/chrome/browser/extensions/convert_user_script_unittest.cc @@ -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))); +} diff --git a/chrome/browser/extensions/crx_installer.cc b/chrome/browser/extensions/crx_installer.cc index 6c6259f..cb5b4e4 100644 --- a/chrome/browser/extensions/crx_installer.cc +++ b/chrome/browser/extensions/crx_installer.cc @@ -11,6 +11,7 @@ #include "base/task.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/chrome_thread.h" +#include "chrome/browser/extensions/convert_user_script.h" #include "chrome/browser/extensions/extension_file_util.h" #include "chrome/common/extensions/extension_error_reporter.h" #include "chrome/common/notification_service.h" @@ -31,41 +32,56 @@ void CrxInstaller::Start(const FilePath& crx_path, const FilePath& install_directory, Extension::Location install_source, const std::string& expected_id, - bool delete_crx, + bool delete_source, bool allow_privilege_increase, ExtensionsService* frontend, ExtensionInstallUI* client) { // Note: We don't keep a reference because this object manages its own // lifetime. - new CrxInstaller(crx_path, install_directory, install_source, expected_id, - delete_crx, allow_privilege_increase, frontend, client); + CrxInstaller* installer = new CrxInstaller(crx_path, install_directory, + delete_source, frontend, + client); + installer->install_source_ = install_source; + installer->expected_id_ = expected_id; + installer->allow_privilege_increase_ = allow_privilege_increase; + + installer->unpacker_ = new SandboxedExtensionUnpacker( + installer->source_file_, g_browser_process->resource_dispatcher_host(), + installer); + + ChromeThread::PostTask( + ChromeThread::FILE, FROM_HERE, + NewRunnableMethod(installer->unpacker_, &SandboxedExtensionUnpacker::Start)); +} + +void CrxInstaller::InstallUserScript(const FilePath& source_file, + const GURL& original_url, + const FilePath& install_directory, + bool delete_source, + ExtensionsService* frontend, + ExtensionInstallUI* client) { + CrxInstaller* installer = new CrxInstaller(source_file, install_directory, + delete_source, frontend, client); + installer->original_url_ = original_url; + + ChromeThread::PostTask( + ChromeThread::FILE, FROM_HERE, + NewRunnableMethod(installer, &CrxInstaller::ConvertUserScriptOnFileThread)); } -CrxInstaller::CrxInstaller(const FilePath& crx_path, +CrxInstaller::CrxInstaller(const FilePath& source_file, const FilePath& install_directory, - Extension::Location install_source, - const std::string& expected_id, - bool delete_crx, - bool allow_privilege_increase, + bool delete_source, ExtensionsService* frontend, ExtensionInstallUI* client) - : crx_path_(crx_path), + : source_file_(source_file), install_directory_(install_directory), - install_source_(install_source), - expected_id_(expected_id), - delete_crx_(delete_crx), - allow_privilege_increase_(allow_privilege_increase), + install_source_(Extension::INTERNAL), + delete_source_(delete_source), + allow_privilege_increase_(false), frontend_(frontend), client_(client) { - extensions_enabled_ = frontend_->extensions_enabled(); - - unpacker_ = new SandboxedExtensionUnpacker( - crx_path, g_browser_process->resource_dispatcher_host(), this); - - ChromeThread::PostTask( - ChromeThread::FILE, FROM_HERE, - NewRunnableMethod(unpacker_, &SandboxedExtensionUnpacker::Start)); } CrxInstaller::~CrxInstaller() { @@ -78,11 +94,23 @@ CrxInstaller::~CrxInstaller() { NewRunnableFunction(&DeleteFileHelper, temp_dir_, true)); } - if (delete_crx_) { + if (delete_source_) { ChromeThread::PostTask( ChromeThread::FILE, FROM_HERE, - NewRunnableFunction(&DeleteFileHelper, crx_path_, false)); + NewRunnableFunction(&DeleteFileHelper, source_file_, false)); + } +} + +void CrxInstaller::ConvertUserScriptOnFileThread() { + std::string error; + Extension* extension = ConvertUserScriptToExtension(source_file_, + original_url_, &error); + if (!extension) { + ReportFailureFromFileThread(error); + return; } + + OnUnpackSuccess(extension->path(), extension->path(), extension); } void CrxInstaller::OnUnpackFailure(const std::string& error_message) { @@ -102,7 +130,6 @@ void CrxInstaller::OnUnpackSuccess(const FilePath& temp_dir, // The unpack dir we don't have to delete explicity since it is a child of // the temp dir. unpacked_extension_root_ = extension_dir; - DCHECK(file_util::ContainsPath(temp_dir_, unpacked_extension_root_)); // Determine whether to allow installation. We always allow themes and // external installs. diff --git a/chrome/browser/extensions/crx_installer.h b/chrome/browser/extensions/crx_installer.h index a0e50ef..bd78cf6 100644 --- a/chrome/browser/extensions/crx_installer.h +++ b/chrome/browser/extensions/crx_installer.h @@ -38,7 +38,7 @@ class CrxInstaller : public SandboxedExtensionUnpackerClient, public ExtensionInstallUI::Delegate { public: - // Starts the installation of the crx file in |crx_path| into + // Starts the installation of the crx file in |source_file| into // |install_directory|. // // Other params: @@ -52,15 +52,25 @@ class CrxInstaller : // client: Optional. If specified, will be used to confirm installation and // also notified of success/fail. Note that we hold a reference to // this, so it can outlive its creator (eg the UI). - static void Start(const FilePath& crx_path, + static void Start(const FilePath& source_file, const FilePath& install_directory, Extension::Location install_source, const std::string& expected_id, - bool delete_crx, + bool delete_source, bool allow_privilege_increase, ExtensionsService* frontend, ExtensionInstallUI* client); + // Starts the installation of the user script file in |source_file| into + // |install_directory|. The script will be converted to an extension. + // See Start() for argument descriptions. + static void InstallUserScript(const FilePath& source_file, + const GURL& original_url, + const FilePath& install_directory, + bool delete_source, + ExtensionsService* frontend, + ExtensionInstallUI* client); + // Given the path to the large icon from an extension, read it if present and // decode it into result. static void DecodeInstallIcon(const FilePath& large_icon_path, @@ -71,16 +81,16 @@ class CrxInstaller : virtual void AbortInstall(); private: - CrxInstaller(const FilePath& crx_path, + CrxInstaller(const FilePath& source_file, const FilePath& install_directory, - Extension::Location install_source, - const std::string& expected_id, - bool delete_crx, - bool allow_privilege_increase, + bool delete_source, ExtensionsService* frontend, ExtensionInstallUI* client); ~CrxInstaller(); + // Converts the source user script to an extension. + void ConvertUserScriptOnFileThread(); + // SandboxedExtensionUnpackerClient virtual void OnUnpackFailure(const std::string& error_message); virtual void OnUnpackSuccess(const FilePath& temp_dir, @@ -103,8 +113,11 @@ class CrxInstaller : void ReportSuccessFromFileThread(); void ReportSuccessFromUIThread(); - // The crx file we're installing. - FilePath crx_path_; + // The file we're installing. + FilePath source_file_; + + // The URL the file was downloaded from. Only used for user scripts. + GURL original_url_; // The directory extensions are installed to. FilePath install_directory_; @@ -123,8 +136,8 @@ class CrxInstaller : // allowed. bool extensions_enabled_; - // Whether we're supposed to delete the source crx file on destruction. - bool delete_crx_; + // Whether we're supposed to delete the source file on destruction. + bool delete_source_; // Whether privileges should be allowed to silently increaes from any // previously installed version of the extension. diff --git a/chrome/browser/extensions/user_script_master.cc b/chrome/browser/extensions/user_script_master.cc index 69fd746..c9228ff 100644 --- a/chrome/browser/extensions/user_script_master.cc +++ b/chrome/browser/extensions/user_script_master.cc @@ -51,7 +51,11 @@ bool UserScriptMaster::ScriptReloader::ParseMetadataHeader( static const base::StringPiece kUserScriptBegin("// ==UserScript=="); static const base::StringPiece kUserScriptEng("// ==/UserScript=="); + static const base::StringPiece kNamespaceDeclaration("// @namespace "); + static const base::StringPiece kNameDeclaration("// @name "); + static const base::StringPiece kDescriptionDeclaration("// @description "); static const base::StringPiece kIncludeDeclaration("// @include "); + static const base::StringPiece kExcludeDeclaration("// @exclude "); static const base::StringPiece kMatchDeclaration("// @match "); static const base::StringPiece kRunAtDeclaration("// @run-at "); static const base::StringPiece kRunAtDocumentStartValue("document-start"); @@ -79,6 +83,16 @@ bool UserScriptMaster::ScriptReloader::ParseMetadataHeader( ReplaceSubstringsAfterOffset(&value, 0, "\\", "\\\\"); ReplaceSubstringsAfterOffset(&value, 0, "?", "\\?"); script->add_glob(value); + } else if (GetDeclarationValue(line, kExcludeDeclaration, &value)) { + ReplaceSubstringsAfterOffset(&value, 0, "\\", "\\\\"); + ReplaceSubstringsAfterOffset(&value, 0, "?", "\\?"); + script->add_exclude_glob(value); + } else if (GetDeclarationValue(line, kNamespaceDeclaration, &value)) { + script->set_name_space(value); + } else if (GetDeclarationValue(line, kNameDeclaration, &value)) { + script->set_name(value); + } else if (GetDeclarationValue(line, kDescriptionDeclaration, &value)) { + script->set_description(value); } else if (GetDeclarationValue(line, kMatchDeclaration, &value)) { URLPattern pattern; if (!pattern.Parse(value)) @@ -97,10 +111,6 @@ bool UserScriptMaster::ScriptReloader::ParseMetadataHeader( line_start = line_end + 1; } - // It is probably a mistake to declare both @include and @match rules. - if (script->globs().size() > 0 && script->url_patterns().size() > 0) - return false; - // If no patterns were specified, default to @include *. This is what // Greasemonkey does. if (script->globs().size() == 0 && script->url_patterns().size() == 0) diff --git a/chrome/browser/extensions/user_script_master.h b/chrome/browser/extensions/user_script_master.h index a062448..270a690 100644 --- a/chrome/browser/extensions/user_script_master.h +++ b/chrome/browser/extensions/user_script_master.h @@ -61,6 +61,7 @@ class UserScriptMaster : public base::RefCountedThreadSafe<UserScriptMaster>, FRIEND_TEST(UserScriptMasterTest, Parse5); FRIEND_TEST(UserScriptMasterTest, Parse6); + public: // We reload user scripts on the file thread to prevent blocking the UI. // ScriptReloader lives on the file thread and does the reload // work, and then sends a message back to its master with a new SharedMemory*. @@ -118,6 +119,7 @@ class UserScriptMaster : public base::RefCountedThreadSafe<UserScriptMaster>, DISALLOW_COPY_AND_ASSIGN(ScriptReloader); }; + private: // DirectoryWatcher::Delegate implementation. virtual void OnDirectoryChanged(const FilePath& path); diff --git a/chrome/browser/extensions/user_script_master_unittest.cc b/chrome/browser/extensions/user_script_master_unittest.cc index 840d8a3..29faaea 100644 --- a/chrome/browser/extensions/user_script_master_unittest.cc +++ b/chrome/browser/extensions/user_script_master_unittest.cc @@ -213,8 +213,8 @@ TEST_F(UserScriptMasterTest, Parse6) { "// @match \t http://mail.yahoo.com/*\n" "// ==/UserScript==\n"); - // Not allowed to mix @include and @value. + // Allowed to match @include and @match. UserScript script; - EXPECT_FALSE(UserScriptMaster::ScriptReloader::ParseMetadataHeader( + EXPECT_TRUE(UserScriptMaster::ScriptReloader::ParseMetadataHeader( text, &script)); } diff --git a/chrome/browser/renderer_host/buffered_resource_handler.cc b/chrome/browser/renderer_host/buffered_resource_handler.cc index 6022e1f..f3ca82f 100644 --- a/chrome/browser/renderer_host/buffered_resource_handler.cc +++ b/chrome/browser/renderer_host/buffered_resource_handler.cc @@ -14,6 +14,7 @@ #include "chrome/browser/renderer_host/resource_dispatcher_host.h" #include "chrome/browser/renderer_host/resource_dispatcher_host_request_info.h" #include "chrome/browser/renderer_host/x509_user_cert_resource_handler.h" +#include "chrome/common/extensions/user_script.h" #include "chrome/common/url_constants.h" #include "net/base/io_buffer.h" #include "net/base/mime_sniffer.h" @@ -441,6 +442,10 @@ bool BufferedResourceHandler::ShouldDownload(bool* need_plugin_list) { return true; } + // Special-case user scripts to get downloaded instead of viewed. + if (UserScript::HasUserScriptFileExtension(request_->url())) + return true; + // MIME type checking. if (net::IsSupportedMimeType(type)) return false; diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index 8b4a857..e8c92f5 100755 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -1317,6 +1317,8 @@ 'browser/download/save_types.h', 'browser/encoding_menu_controller.cc', 'browser/encoding_menu_controller.h', + 'browser/extensions/convert_user_script.cc', + 'browser/extensions/convert_user_script.h', 'browser/extensions/crashed_extension_infobar.cc', 'browser/extensions/crashed_extension_infobar.h', 'browser/extensions/crx_installer.cc', @@ -4551,6 +4553,7 @@ 'browser/download/download_request_manager_unittest.cc', 'browser/download/save_package_unittest.cc', 'browser/encoding_menu_controller_unittest.cc', + 'browser/extensions/convert_user_script_unittest.cc', 'browser/extensions/extension_file_util_unittest.cc', 'browser/extensions/extension_messages_unittest.cc', 'browser/extensions/extension_process_manager_unittest.cc', diff --git a/chrome/common/extensions/extension.cc b/chrome/common/extensions/extension.cc index c617232..0aad3de 100644 --- a/chrome/common/extensions/extension.cc +++ b/chrome/common/extensions/extension.cc @@ -248,6 +248,17 @@ bool Extension::LoadUserScriptHelper(const DictionaryValue* content_script, result->add_url_pattern(pattern); } + // include/exclude globs (mostly for Greasemonkey compat) + if (!LoadGlobsHelper(content_script, definition_index, keys::kIncludeGlobs, + error, &UserScript::add_glob, result)) { + return false; + } + + if (!LoadGlobsHelper(content_script, definition_index, keys::kExcludeGlobs, + error, &UserScript::add_exclude_glob, result)) { + return false; + } + // js and css keys ListValue* js = NULL; if (content_script->HasKey(keys::kJs) && @@ -311,6 +322,38 @@ bool Extension::LoadUserScriptHelper(const DictionaryValue* content_script, return true; } +bool Extension::LoadGlobsHelper( + const DictionaryValue* content_script, + int content_script_index, + const wchar_t* globs_property_name, + std::string* error, + void (UserScript::*add_method) (const std::string& glob), + UserScript *instance) { + if (!content_script->HasKey(globs_property_name)) + return true; // they are optional + + ListValue* list = NULL; + if (!content_script->GetList(globs_property_name, &list)) { + *error = ExtensionErrorUtils::FormatErrorMessage(errors::kInvalidGlobList, + IntToString(content_script_index), WideToASCII(globs_property_name)); + return false; + } + + for (size_t i = 0; i < list->GetSize(); ++i) { + std::string glob; + if (!list->GetString(i, &glob)) { + *error = ExtensionErrorUtils::FormatErrorMessage(errors::kInvalidGlob, + IntToString(content_script_index), WideToASCII(globs_property_name), + IntToString(i)); + return false; + } + + (instance->*add_method)(glob); + } + + return true; +} + ExtensionAction* Extension::LoadExtensionActionHelper( const DictionaryValue* extension_action, std::string* error) { scoped_ptr<ExtensionAction> result(new ExtensionAction()); @@ -450,7 +493,8 @@ ExtensionResource Extension::GetResource(const FilePath& extension_path, } Extension::Extension(const FilePath& path) - : is_theme_(false), background_page_ready_(false) { + : converted_from_user_script_(false), is_theme_(false), + background_page_ready_(false) { DCHECK(path.IsAbsolute()); location_ = INVALID; @@ -668,6 +712,10 @@ bool Extension::InitFromValue(const DictionaryValue& source, bool require_id, } } + // Initialize converted_from_user_script (if present) + source.GetBoolean(keys::kConvertedFromUserScript, + &converted_from_user_script_); + // Initialize icons (if present). if (source.HasKey(keys::kIcons)) { DictionaryValue* icons_value = NULL; @@ -943,6 +991,8 @@ bool Extension::InitFromValue(const DictionaryValue& source, bool require_id, if (!LoadUserScriptHelper(content_script, i, error, &script)) return false; // Failed to parse script context definition script.set_extension_id(id()); + if (converted_from_user_script_) + script.set_emulate_greasemonkey(true); content_scripts_.push_back(script); } } diff --git a/chrome/common/extensions/extension.h b/chrome/common/extensions/extension.h index 050515b..d240388 100644 --- a/chrome/common/extensions/extension.h +++ b/chrome/common/extensions/extension.h @@ -195,6 +195,9 @@ class Extension { const std::string& name() const { return name_; } const std::string& public_key() const { return public_key_; } const std::string& description() const { return description_; } + bool converted_from_user_script() const { + return converted_from_user_script_; + } const UserScriptList& content_scripts() const { return content_scripts_; } ExtensionAction* page_action() const { return page_action_.get(); } ExtensionAction* browser_action() const { return browser_action_.get(); } @@ -290,6 +293,15 @@ class Extension { std::string* error, UserScript* result); + // Helper method that loads either the include_globs or exclude_globs list + // from an entry in the content_script lists of the manifest. + bool LoadGlobsHelper(const DictionaryValue* content_script, + int content_script_index, + const wchar_t* globs_property_name, + std::string* error, + void (UserScript::*add_method) (const std::string& glob), + UserScript *instance); + // Helper method to load an ExtensionAction from the page_action or // browser_action entries in the manifest. ExtensionAction* LoadExtensionActionHelper( @@ -325,6 +337,10 @@ class Extension { // An optional longer description of the extension. std::string description_; + // True if the extension was generated from a user script. (We show slightly + // different UI if so). + bool converted_from_user_script_; + // Paths to the content scripts the extension contains. UserScriptList content_scripts_; diff --git a/chrome/common/extensions/extension_constants.cc b/chrome/common/extensions/extension_constants.cc index 59e7c14..0dbab1f 100644 --- a/chrome/common/extensions/extension_constants.cc +++ b/chrome/common/extensions/extension_constants.cc @@ -10,12 +10,15 @@ const wchar_t* kBackground = L"background_page"; const wchar_t* kBrowserAction = L"browser_action"; const wchar_t* kChromeURLOverrides = L"chrome_url_overrides"; const wchar_t* kContentScripts = L"content_scripts"; +const wchar_t* kConvertedFromUserScript = L"converted_from_user_script"; const wchar_t* kCss = L"css"; const wchar_t* kDefaultLocale = L"default_locale"; const wchar_t* kDescription = L"description"; const wchar_t* kIcons = L"icons"; const wchar_t* kJs = L"js"; const wchar_t* kMatches = L"matches"; +const wchar_t* kIncludeGlobs = L"include_globs"; +const wchar_t* kExcludeGlobs = L"exclude_globs"; const wchar_t* kName = L"name"; const wchar_t* kPageActionId = L"id"; const wchar_t* kPageAction = L"page_action"; @@ -76,6 +79,10 @@ const char* kInvalidCssList = "Required value 'content_scripts[*].css is invalid."; const char* kInvalidDescription = "Invalid value for 'description'."; +const char* kInvalidGlobList = + "Invalid value for 'content_scripts[*].*'."; +const char* kInvalidGlob = + "Invalid value for 'content_scripts[*].*[*]'."; const char* kInvalidIcons = "Invalid value for 'icons'."; const char* kInvalidIconPath = diff --git a/chrome/common/extensions/extension_constants.h b/chrome/common/extensions/extension_constants.h index b7826a7..d44f7b9 100644 --- a/chrome/common/extensions/extension_constants.h +++ b/chrome/common/extensions/extension_constants.h @@ -11,10 +11,13 @@ namespace extension_manifest_keys { extern const wchar_t* kBrowserAction; extern const wchar_t* kChromeURLOverrides; extern const wchar_t* kContentScripts; + extern const wchar_t* kConvertedFromUserScript; extern const wchar_t* kCss; extern const wchar_t* kDefaultLocale; extern const wchar_t* kDescription; + extern const wchar_t* kExcludeGlobs; extern const wchar_t* kIcons; + extern const wchar_t* kIncludeGlobs; extern const wchar_t* kJs; extern const wchar_t* kMatches; extern const wchar_t* kName; @@ -70,6 +73,8 @@ namespace extension_manifest_errors { extern const char* kInvalidDescription; extern const char* kInvalidIcons; extern const char* kInvalidIconPath; + extern const char* kInvalidGlobList; + extern const char* kInvalidGlob; extern const char* kInvalidJs; extern const char* kInvalidJsList; extern const char* kInvalidKey; diff --git a/chrome/common/extensions/extension_error_utils.cc b/chrome/common/extensions/extension_error_utils.cc index 724cdcd..0256c8d8 100644 --- a/chrome/common/extensions/extension_error_utils.cc +++ b/chrome/common/extensions/extension_error_utils.cc @@ -8,7 +8,7 @@ std::string ExtensionErrorUtils::FormatErrorMessage( const std::string& format, - const std::string s1) { + const std::string& s1) { std::string ret_val = format; ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s1); return ret_val; @@ -16,10 +16,22 @@ std::string ExtensionErrorUtils::FormatErrorMessage( std::string ExtensionErrorUtils::FormatErrorMessage( const std::string& format, - const std::string s1, - const std::string s2) { + const std::string& s1, + const std::string& s2) { std::string ret_val = format; ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s1); ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s2); return ret_val; } + +std::string ExtensionErrorUtils::FormatErrorMessage( + const std::string& format, + const std::string& s1, + const std::string& s2, + const std::string& s3) { + std::string ret_val = format; + ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s1); + ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s2); + ReplaceFirstSubstringAfterOffset(&ret_val, 0, "*", s3); + return ret_val; +} diff --git a/chrome/common/extensions/extension_error_utils.h b/chrome/common/extensions/extension_error_utils.h index f9f1e7c..fdd9b03 100644 --- a/chrome/common/extensions/extension_error_utils.h +++ b/chrome/common/extensions/extension_error_utils.h @@ -9,16 +9,18 @@ class ExtensionErrorUtils { public: - // Creates an error messages from a pattern. Places first instance if "*" - // with |s1|. + // Creates an error messages from a pattern. static std::string FormatErrorMessage(const std::string& format, - const std::string s1); + const std::string& s1); - // Creates an error messages from a pattern. Places first instance if "*" - // with |s1| and second instance of "*" with |s2|. static std::string FormatErrorMessage(const std::string& format, - const std::string s1, - const std::string s2); + const std::string& s1, + const std::string& s2); + + static std::string FormatErrorMessage(const std::string& format, + const std::string& s1, + const std::string& s2, + const std::string& s3); }; #endif // CHROME_COMMON_EXTENSIONS_EXTENSION_ERROR_UTILS_H_ diff --git a/chrome/common/extensions/user_script.cc b/chrome/common/extensions/user_script.cc index df34e52..10d0b60 100644 --- a/chrome/common/extensions/user_script.cc +++ b/chrome/common/extensions/user_script.cc @@ -7,21 +7,59 @@ #include "base/pickle.h" #include "base/string_util.h" -bool UserScript::MatchesUrl(const GURL& url) const { - for (std::vector<std::string>::const_iterator glob = globs_.begin(); - glob != globs_.end(); ++glob) { - if (MatchPattern(url.spec(), *glob)) +namespace { +static bool UrlMatchesPatterns(const UserScript::PatternList* patterns, + const GURL& url) { + for (UserScript::PatternList::const_iterator pattern = patterns->begin(); + pattern != patterns->end(); ++pattern) { + if (pattern->MatchesUrl(url)) return true; } - for (PatternList::const_iterator pattern = url_patterns_.begin(); - pattern != url_patterns_.end(); ++pattern) { - if (pattern->MatchesUrl(url)) + return false; +} + +static bool UrlMatchesGlobs(const std::vector<std::string>* globs, + const GURL& url) { + for (std::vector<std::string>::const_iterator glob = globs->begin(); + glob != globs->end(); ++glob) { + if (MatchPattern(url.spec(), *glob)) return true; } return false; } +} + +const char UserScript::kFileExtension[] = ".user.js"; + +bool UserScript::HasUserScriptFileExtension(const GURL& url) { + return EndsWith(url.ExtractFileName(), kFileExtension, false); +} + +bool UserScript::HasUserScriptFileExtension(const FilePath& path) { + static FilePath extension(FilePath().AppendASCII(kFileExtension)); + return EndsWith(path.BaseName().value(), extension.value(), false); +} + +bool UserScript::MatchesUrl(const GURL& url) const { + if (url_patterns_.size() > 0) { + if (!UrlMatchesPatterns(&url_patterns_, url)) + return false; + } + + if (globs_.size() > 0) { + if (!UrlMatchesGlobs(&globs_, url)) + return false; + } + + if (exclude_globs_.size() > 0) { + if (UrlMatchesGlobs(&exclude_globs_, url)) + return false; + } + + return true; +} void UserScript::File::Pickle(::Pickle* pickle) const { pickle->WriteString(url_.spec()); @@ -43,10 +81,17 @@ void UserScript::Pickle(::Pickle* pickle) const { // Write the extension id. pickle->WriteString(extension_id()); + // Write Greasemonkey emulation. + pickle->WriteBool(emulate_greasemonkey()); + // Write globs. + std::vector<std::string>::const_iterator glob; pickle->WriteSize(globs_.size()); - for (std::vector<std::string>::const_iterator glob = globs_.begin(); - glob != globs_.end(); ++glob) { + for (glob = globs_.begin(); glob != globs_.end(); ++glob) { + pickle->WriteString(*glob); + } + pickle->WriteSize(exclude_globs_.size()); + for (glob = exclude_globs_.begin(); glob != exclude_globs_.end(); ++glob) { pickle->WriteString(*glob); } @@ -82,10 +127,12 @@ void UserScript::Unpickle(const ::Pickle& pickle, void** iter) { // Read the extension ID. CHECK(pickle.ReadString(iter, &extension_id_)); + // Read Greasemonkey emulation. + CHECK(pickle.ReadBool(iter, &emulate_greasemonkey_)); + // Read globs. size_t num_globs = 0; CHECK(pickle.ReadSize(iter, &num_globs)); - globs_.clear(); for (size_t i = 0; i < num_globs; ++i) { std::string glob; @@ -93,6 +140,14 @@ void UserScript::Unpickle(const ::Pickle& pickle, void** iter) { globs_.push_back(glob); } + CHECK(pickle.ReadSize(iter, &num_globs)); + exclude_globs_.clear(); + for (size_t i = 0; i < num_globs; ++i) { + std::string glob; + CHECK(pickle.ReadString(iter, &glob)); + exclude_globs_.push_back(glob); + } + // Read url patterns. size_t num_patterns = 0; CHECK(pickle.ReadSize(iter, &num_patterns)); diff --git a/chrome/common/extensions/user_script.h b/chrome/common/extensions/user_script.h index bdd1e54..adcfa3f 100644 --- a/chrome/common/extensions/user_script.h +++ b/chrome/common/extensions/user_script.h @@ -22,6 +22,13 @@ class UserScript { public: typedef std::vector<URLPattern> PatternList; + // The file extension for standalone user scripts. + static const char kFileExtension[]; + + // Check if a file or URL has the user script file extension. + static bool HasUserScriptFileExtension(const GURL& url); + static bool HasUserScriptFileExtension(const FilePath& path); + // Locations that user scripts can be run inside the document. enum RunLocation { DOCUMENT_START, // After the documentElemnet is created, but before @@ -92,20 +99,45 @@ class UserScript { typedef std::vector<File> FileList; - // Constructor. Default the run location to document idle, which is similar - // to Greasemonkey but should result in better page load times for fast- - // loading pages. - UserScript() : run_location_(DOCUMENT_IDLE) {} + // Constructor. Default the run location to document end, which is like + // Greasemonkey and probably more useful for typical scripts. + UserScript() + : run_location_(DOCUMENT_IDLE), emulate_greasemonkey_(false) { + } + + const std::string& name_space() const { return name_space_; } + void set_name_space(const std::string& name_space) { + name_space_ = name_space; + } + + const std::string& name() const { return name_; } + void set_name(const std::string& name) { name_ = name; } + + const std::string& description() const { return description_; } + void set_description(const std::string& description) { + description_ = description; + } // The place in the document to run the script. RunLocation run_location() const { return run_location_; } void set_run_location(RunLocation location) { run_location_ = location; } + // Whether to emulate greasemonkey when running this script. + bool emulate_greasemonkey() const { return emulate_greasemonkey_; } + void set_emulate_greasemonkey(bool val) { emulate_greasemonkey_ = val; } + // The globs, if any, that determine which pages this script runs against. // These are only used with "standalone" Greasemonkey-like user scripts. const std::vector<std::string>& globs() const { return globs_; } void add_glob(const std::string& glob) { globs_.push_back(glob); } void clear_globs() { globs_.clear(); } + const std::vector<std::string>& exclude_globs() const { + return exclude_globs_; + } + void add_exclude_glob(const std::string& glob) { + exclude_globs_.push_back(glob); + } + void clear_exclude_globs() { exclude_globs_.clear(); } // The URLPatterns, if any, that determine which pages this script runs // against. @@ -145,9 +177,20 @@ class UserScript { // The location to run the script inside the document. RunLocation run_location_; + // The namespace of the script. This is used by Greasemonkey in the same way + // as XML namespaces. Only used when parsing Greasemonkey-style scripts. + std::string name_space_; + + // The script's name. Only used when parsing Greasemonkey-style scripts. + std::string name_; + + // A longer description. Only used when parsing Greasemonkey-style scripts. + std::string description_; + // Greasemonkey-style globs that determine pages to inject the script into. // These are only used with standalone scripts. std::vector<std::string> globs_; + std::vector<std::string> exclude_globs_; // URLPatterns that determine pages to inject the script into. These are // only used with scripts that are part of extensions. @@ -162,6 +205,10 @@ class UserScript { // The ID of the extension this script is a part of, if any. Can be empty if // the script is a "standlone" user script. std::string extension_id_; + + // Whether we should try to emulate Greasemonkey's APIs when running this + // script. + bool emulate_greasemonkey_; }; typedef std::vector<UserScript> UserScriptList; diff --git a/chrome/common/extensions/user_script_unittest.cc b/chrome/common/extensions/user_script_unittest.cc index 58ef77e..7b5cc6d 100644 --- a/chrome/common/extensions/user_script_unittest.cc +++ b/chrome/common/extensions/user_script_unittest.cc @@ -21,6 +21,10 @@ TEST(UserScriptTest, Match1) { EXPECT_TRUE(script.MatchesUrl(GURL("http://mail.yahoo.com/bar"))); EXPECT_TRUE(script.MatchesUrl(GURL("http://mail.msn.com/baz"))); EXPECT_FALSE(script.MatchesUrl(GURL("http://www.hotmail.com"))); + + script.add_exclude_glob("*foo*"); + EXPECT_TRUE(script.MatchesUrl(GURL("http://mail.google.com"))); + EXPECT_FALSE(script.MatchesUrl(GURL("http://mail.google.com/foo"))); } TEST(UserScriptTest, Match2) { @@ -70,6 +74,36 @@ TEST(UserScriptTest, Match6) { // NOTE: URLPattern is tested more extensively in url_pattern_unittest.cc. } +TEST(UserScriptTest, UrlPatternGlobInteraction) { + // If there are both, match intersection(union(globs), union(urlpatterns)). + UserScript script; + + URLPattern pattern; + ASSERT_TRUE(pattern.Parse("http://www.google.com/*")); + script.add_url_pattern(pattern); + + script.add_glob("*bar*"); + + // No match, because it doesn't match the glob. + EXPECT_FALSE(script.MatchesUrl(GURL("http://www.google.com/foo"))); + + script.add_exclude_glob("*baz*"); + + // No match, because it matches the exclude glob. + EXPECT_FALSE(script.MatchesUrl(GURL("http://www.google.com/baz"))); + + // Match, because it matches the glob, doesn't match the exclude glob. + EXPECT_TRUE(script.MatchesUrl(GURL("http://www.google.com/bar"))); + + // Try with just a single exclude glob. + script.clear_globs(); + EXPECT_TRUE(script.MatchesUrl(GURL("http://www.google.com/foo"))); + + // Try with no globs or exclude globs. + script.clear_exclude_globs(); + EXPECT_TRUE(script.MatchesUrl(GURL("http://www.google.com/foo"))); +} + TEST(UserScriptTest, Pickle) { URLPattern pattern1; URLPattern pattern2; diff --git a/chrome/renderer/resources/greasemonkey_api.js b/chrome/renderer/resources/greasemonkey_api.js index 7b829ad..52f7560 100644 --- a/chrome/renderer/resources/greasemonkey_api.js +++ b/chrome/renderer/resources/greasemonkey_api.js @@ -67,3 +67,12 @@ function GM_openInTab(url) { function GM_log(message) { window.console.log(message); } + +(function() { + var apis = ["GM_getValue", "GM_setValue", "GM_registerMenuCommand"]; + for (var i = 0, api; api = apis[i]; i++) { + window[api] = function() { + console.log("%s is not supported.", api); + } + } +})(); diff --git a/chrome/renderer/user_script_slave.cc b/chrome/renderer/user_script_slave.cc index b71c7a8..473cda8 100644 --- a/chrome/renderer/user_script_slave.cc +++ b/chrome/renderer/user_script_slave.cc @@ -51,22 +51,9 @@ int UserScriptSlave::GetIsolatedWorldId(const std::string& extension_id) { UserScriptSlave::UserScriptSlave() : shared_memory_(NULL), - script_deleter_(&scripts_), - user_script_start_line_(0) { + script_deleter_(&scripts_) { api_js_ = ResourceBundle::GetSharedInstance().GetRawDataResource( IDR_GREASEMONKEY_API_JS); - - // Count the number of lines that will be injected before the user script. - base::StringPiece::size_type pos = 0; - while ((pos = api_js_.find('\n', pos)) != base::StringPiece::npos) { - user_script_start_line_++; - pos++; - } - - // NOTE: There is actually one extra line in the injected script because the - // function header includes a newline as well. But WebKit expects the - // numbering to be one-based, not zero-based, so actually *not* accounting for - // this extra line ends us up with the right offset. } bool UserScriptSlave::UpdateScripts(base::SharedMemoryHandle shared_memory) { @@ -178,13 +165,16 @@ bool UserScriptSlave::InjectScripts(WebFrame* frame, if (!sources.empty()) { int isolated_world_id = 0; - if (script->is_standalone()) { - // For standalone scripts, we try to emulate the Greasemonkey API. + // Emulate Greasemonkey API for scripts that were converted to extensions + // and "standalone" user scripts. + if (script->is_standalone() || script->emulate_greasemonkey()) { sources.insert(sources.begin(), WebScriptSource(WebString::fromUTF8(api_js_.as_string()))); - } else { - // Setup chrome.self to contain an Extension object with the correct - // ID. + } + + // Setup chrome.self to contain an Extension object with the correct + // ID. + if (!script->extension_id().empty()) { InsertInitExtensionCode(&sources, script->extension_id()); isolated_world_id = GetIsolatedWorldId(script->extension_id()); } diff --git a/chrome/renderer/user_script_slave.h b/chrome/renderer/user_script_slave.h index 8232ec4..3d7c49d 100644 --- a/chrome/renderer/user_script_slave.h +++ b/chrome/renderer/user_script_slave.h @@ -50,11 +50,6 @@ class UserScriptSlave { // Greasemonkey API source that is injected with the scripts. base::StringPiece api_js_; - // The line number of the first line of the user script among all of the - // injected javascript. This is used to make reported errors correspond with - // the proper line in the user script. - int user_script_start_line_; - DISALLOW_COPY_AND_ASSIGN(UserScriptSlave); }; diff --git a/chrome/test/data/extensions/user_script_basic.user.js b/chrome/test/data/extensions/user_script_basic.user.js new file mode 100755 index 0000000..ea4f8a0 --- /dev/null +++ b/chrome/test/data/extensions/user_script_basic.user.js @@ -0,0 +1,11 @@ +// ==UserScript==
+// @name My user script
+// @namespace http://www.google.com
+// @description Does totally awesome stuff.
+// @include http://www.google.com/*
+// @include http://www.yahoo.com/*
+// @exclude *foo*
+// @match http://www.google.com/*
+// ==/UserScript==
+
+alert("Hello! This is my script.");
diff --git a/chrome/test/data/extensions/user_script_no_metadata.user.js b/chrome/test/data/extensions/user_script_no_metadata.user.js new file mode 100755 index 0000000..f8fe23e --- /dev/null +++ b/chrome/test/data/extensions/user_script_no_metadata.user.js @@ -0,0 +1 @@ +alert("This user script has no metadata, but it is also valid!");
|