From: uazo Date: Fri, 13 Aug 2021 17:10:47 +0000 Subject: Experimental user scripts support Activate userscripts as in the desktop version of chromium. is possible to add them in two ways: by selecting files from the picker in the settings or downloading the scripts and opening it from downloads (only if ends with .user.js) new imported scripts are disabled by default: they can be activated via the ui. enabled headers are: name, version, description, include, exclude, match, exclude_match (only http and https), run-at (document-start, document-end, document-idle), homepage, url_source ui, among other things, allows you to see the source of the script. needs add-support-for-ISupportHelpAndFeedback.patch see also: README.md --- chrome/android/BUILD.gn | 5 + .../android/java/res/xml/main_preferences.xml | 5 + .../browser/download/DownloadUtils.java | 6 + .../init/ProcessInitializationHandler.java | 3 + chrome/android/java_sources.gni | 3 + chrome/browser/BUILD.gn | 5 + chrome/browser/about_flags.cc | 5 + .../browser/chrome_content_browser_client.cc | 3 +- chrome/browser/flag_descriptions.cc | 5 + chrome/browser/flag_descriptions.h | 3 + chrome/browser/prefs/browser_prefs.cc | 2 + chrome/browser/profiles/BUILD.gn | 1 + ...hrome_browser_main_extra_parts_profiles.cc | 3 + chrome/browser/profiles/profile_manager.cc | 9 + chrome/browser/profiles/renderer_updater.cc | 10 +- chrome/browser/profiles/renderer_updater.h | 1 + .../webui/chrome_web_ui_controller_factory.cc | 3 + chrome/chrome_paks.gni | 2 + chrome/common/renderer_configuration.mojom | 1 + chrome/renderer/BUILD.gn | 1 + .../chrome_content_renderer_client.cc | 37 + .../renderer/chrome_render_thread_observer.cc | 3 + components/components_strings.grd | 1 + components/user_scripts/README.md | 150 ++++ components/user_scripts/android/BUILD.gn | 80 ++ .../java/res/layout/accept_script_item.xml | 160 ++++ .../java/res/layout/accept_script_list.xml | 10 + .../java/res/layout/scripts_preference.xml | 40 + .../android/java/res/values/dimens.xml | 11 + .../java/res/xml/userscripts_preferences.xml | 34 + .../user_scripts/UserScriptsUtils.java | 84 ++ .../user_scripts/FragmentWindowAndroid.java | 89 ++ .../user_scripts/IUserScriptsUtils.java | 22 + .../components/user_scripts/ScriptInfo.java | 37 + .../user_scripts/ScriptListBaseAdapter.java | 163 ++++ .../user_scripts/ScriptListPreference.java | 171 ++++ .../user_scripts/UserScriptsBridge.java | 200 +++++ .../user_scripts/UserScriptsPreferences.java | 116 +++ .../user_scripts/android/java_sources.gni | 18 + .../android/user_scripts_bridge.cc | 173 ++++ .../android/user_scripts_bridge.h | 31 + components/user_scripts/browser/BUILD.gn | 83 ++ .../user_scripts/browser/file_task_runner.cc | 40 + .../user_scripts/browser/file_task_runner.h | 34 + .../browser/resources/browser_resources.grd | 14 + .../browser/resources/user-script-ui/BUILD.gn | 12 + .../user-script-ui/user-scripts-ui.html | 14 + .../user-script-ui/user-scripts-ui.js | 9 + .../browser/ui/user_scripts_ui.cc | 148 ++++ .../user_scripts/browser/ui/user_scripts_ui.h | 39 + .../browser/user_script_loader.cc | 721 ++++++++++++++++ .../user_scripts/browser/user_script_loader.h | 170 ++++ .../browser/user_script_pref_info.cc | 34 + .../browser/user_script_pref_info.h | 72 ++ .../user_scripts/browser/user_script_prefs.cc | 276 ++++++ .../user_scripts/browser/user_script_prefs.h | 62 ++ .../browser/userscripts_browser_client.cc | 78 ++ .../browser/userscripts_browser_client.h | 62 ++ components/user_scripts/common/BUILD.gn | 49 ++ components/user_scripts/common/constants.h | 21 + components/user_scripts/common/error_utils.cc | 54 ++ components/user_scripts/common/error_utils.h | 24 + .../common/extension_message_generator.cc | 29 + .../common/extension_message_generator.h | 11 + .../user_scripts/common/extension_messages.cc | 40 + .../user_scripts/common/extension_messages.h | 71 ++ components/user_scripts/common/host_id.cc | 31 + components/user_scripts/common/host_id.h | 35 + .../user_scripts/common/script_constants.h | 33 + components/user_scripts/common/url_pattern.cc | 803 ++++++++++++++++++ components/user_scripts/common/url_pattern.h | 302 +++++++ .../user_scripts/common/url_pattern_set.cc | 334 ++++++++ .../user_scripts/common/url_pattern_set.h | 161 ++++ components/user_scripts/common/user_script.cc | 317 +++++++ components/user_scripts/common/user_script.h | 403 +++++++++ .../common/user_scripts_features.cc | 32 + .../common/user_scripts_features.h | 34 + components/user_scripts/common/view_type.cc | 39 + components/user_scripts/common/view_type.h | 48 ++ components/user_scripts/renderer/BUILD.gn | 67 ++ .../renderer/extension_frame_helper.cc | 96 +++ .../renderer/extension_frame_helper.h | 92 ++ .../user_scripts/renderer/injection_host.cc | 12 + .../user_scripts/renderer/injection_host.h | 42 + .../renderer/resources/greasemonkey_api.js | 82 ++ .../user_scripts_renderer_resources.grd | 14 + .../user_scripts/renderer/script_context.cc | 215 +++++ .../user_scripts/renderer/script_context.h | 70 ++ .../user_scripts/renderer/script_injection.cc | 343 ++++++++ .../user_scripts/renderer/script_injection.h | 155 ++++ .../renderer/script_injection_callback.cc | 25 + .../renderer/script_injection_callback.h | 39 + .../renderer/script_injection_manager.cc | 417 +++++++++ .../renderer/script_injection_manager.h | 102 +++ .../user_scripts/renderer/script_injector.h | 96 +++ .../user_scripts/renderer/scripts_run_info.cc | 31 + .../user_scripts/renderer/scripts_run_info.h | 70 ++ .../renderer/user_script_injector.cc | 228 +++++ .../renderer/user_script_injector.h | 87 ++ .../user_scripts/renderer/user_script_set.cc | 259 ++++++ .../user_scripts/renderer/user_script_set.h | 102 +++ .../renderer/user_script_set_manager.cc | 77 ++ .../renderer/user_script_set_manager.h | 62 ++ .../renderer/user_scripts_dispatcher.cc | 36 + .../renderer/user_scripts_dispatcher.h | 48 ++ .../renderer/user_scripts_renderer_client.cc | 105 +++ .../renderer/user_scripts_renderer_client.h | 36 + .../renderer/web_ui_injection_host.cc | 40 + .../renderer/web_ui_injection_host.h | 28 + .../strings/userscripts_strings.grdp | 55 ++ tools/gritsettings/resource_ids.spec | 6 + 111 files changed, 9580 insertions(+), 2 deletions(-) create mode 100644 components/user_scripts/README.md create mode 100755 components/user_scripts/android/BUILD.gn create mode 100644 components/user_scripts/android/java/res/layout/accept_script_item.xml create mode 100644 components/user_scripts/android/java/res/layout/accept_script_list.xml create mode 100644 components/user_scripts/android/java/res/layout/scripts_preference.xml create mode 100755 components/user_scripts/android/java/res/values/dimens.xml create mode 100644 components/user_scripts/android/java/res/xml/userscripts_preferences.xml create mode 100755 components/user_scripts/android/java/src/org/chromium/chrome/browser/user_scripts/UserScriptsUtils.java create mode 100644 components/user_scripts/android/java/src/org/chromium/components/user_scripts/FragmentWindowAndroid.java create mode 100644 components/user_scripts/android/java/src/org/chromium/components/user_scripts/IUserScriptsUtils.java create mode 100644 components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptInfo.java create mode 100644 components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptListBaseAdapter.java create mode 100644 components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptListPreference.java create mode 100644 components/user_scripts/android/java/src/org/chromium/components/user_scripts/UserScriptsBridge.java create mode 100755 components/user_scripts/android/java/src/org/chromium/components/user_scripts/UserScriptsPreferences.java create mode 100644 components/user_scripts/android/java_sources.gni create mode 100644 components/user_scripts/android/user_scripts_bridge.cc create mode 100644 components/user_scripts/android/user_scripts_bridge.h create mode 100755 components/user_scripts/browser/BUILD.gn create mode 100755 components/user_scripts/browser/file_task_runner.cc create mode 100755 components/user_scripts/browser/file_task_runner.h create mode 100644 components/user_scripts/browser/resources/browser_resources.grd create mode 100644 components/user_scripts/browser/resources/user-script-ui/BUILD.gn create mode 100644 components/user_scripts/browser/resources/user-script-ui/user-scripts-ui.html create mode 100644 components/user_scripts/browser/resources/user-script-ui/user-scripts-ui.js create mode 100644 components/user_scripts/browser/ui/user_scripts_ui.cc create mode 100644 components/user_scripts/browser/ui/user_scripts_ui.h create mode 100755 components/user_scripts/browser/user_script_loader.cc create mode 100755 components/user_scripts/browser/user_script_loader.h create mode 100644 components/user_scripts/browser/user_script_pref_info.cc create mode 100644 components/user_scripts/browser/user_script_pref_info.h create mode 100644 components/user_scripts/browser/user_script_prefs.cc create mode 100644 components/user_scripts/browser/user_script_prefs.h create mode 100755 components/user_scripts/browser/userscripts_browser_client.cc create mode 100755 components/user_scripts/browser/userscripts_browser_client.h create mode 100755 components/user_scripts/common/BUILD.gn create mode 100755 components/user_scripts/common/constants.h create mode 100755 components/user_scripts/common/error_utils.cc create mode 100755 components/user_scripts/common/error_utils.h create mode 100755 components/user_scripts/common/extension_message_generator.cc create mode 100755 components/user_scripts/common/extension_message_generator.h create mode 100755 components/user_scripts/common/extension_messages.cc create mode 100755 components/user_scripts/common/extension_messages.h create mode 100755 components/user_scripts/common/host_id.cc create mode 100755 components/user_scripts/common/host_id.h create mode 100755 components/user_scripts/common/script_constants.h create mode 100755 components/user_scripts/common/url_pattern.cc create mode 100755 components/user_scripts/common/url_pattern.h create mode 100755 components/user_scripts/common/url_pattern_set.cc create mode 100755 components/user_scripts/common/url_pattern_set.h create mode 100755 components/user_scripts/common/user_script.cc create mode 100755 components/user_scripts/common/user_script.h create mode 100644 components/user_scripts/common/user_scripts_features.cc create mode 100644 components/user_scripts/common/user_scripts_features.h create mode 100755 components/user_scripts/common/view_type.cc create mode 100755 components/user_scripts/common/view_type.h create mode 100755 components/user_scripts/renderer/BUILD.gn create mode 100755 components/user_scripts/renderer/extension_frame_helper.cc create mode 100755 components/user_scripts/renderer/extension_frame_helper.h create mode 100755 components/user_scripts/renderer/injection_host.cc create mode 100755 components/user_scripts/renderer/injection_host.h create mode 100755 components/user_scripts/renderer/resources/greasemonkey_api.js create mode 100755 components/user_scripts/renderer/resources/user_scripts_renderer_resources.grd create mode 100755 components/user_scripts/renderer/script_context.cc create mode 100755 components/user_scripts/renderer/script_context.h create mode 100755 components/user_scripts/renderer/script_injection.cc create mode 100755 components/user_scripts/renderer/script_injection.h create mode 100755 components/user_scripts/renderer/script_injection_callback.cc create mode 100755 components/user_scripts/renderer/script_injection_callback.h create mode 100755 components/user_scripts/renderer/script_injection_manager.cc create mode 100755 components/user_scripts/renderer/script_injection_manager.h create mode 100755 components/user_scripts/renderer/script_injector.h create mode 100755 components/user_scripts/renderer/scripts_run_info.cc create mode 100755 components/user_scripts/renderer/scripts_run_info.h create mode 100755 components/user_scripts/renderer/user_script_injector.cc create mode 100755 components/user_scripts/renderer/user_script_injector.h create mode 100755 components/user_scripts/renderer/user_script_set.cc create mode 100755 components/user_scripts/renderer/user_script_set.h create mode 100755 components/user_scripts/renderer/user_script_set_manager.cc create mode 100755 components/user_scripts/renderer/user_script_set_manager.h create mode 100755 components/user_scripts/renderer/user_scripts_dispatcher.cc create mode 100755 components/user_scripts/renderer/user_scripts_dispatcher.h create mode 100755 components/user_scripts/renderer/user_scripts_renderer_client.cc create mode 100755 components/user_scripts/renderer/user_scripts_renderer_client.h create mode 100755 components/user_scripts/renderer/web_ui_injection_host.cc create mode 100755 components/user_scripts/renderer/web_ui_injection_host.h create mode 100755 components/user_scripts/strings/userscripts_strings.grdp diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn --- a/chrome/android/BUILD.gn +++ b/chrome/android/BUILD.gn @@ -252,6 +252,10 @@ android_resources("chrome_app_java_resources") { "//third_party/androidx:androidx_preference_preference_java", "//third_party/androidx:androidx_recyclerview_recyclerview_java", ] + + # this need to be into android_resources("chrome_app_java_resources") section because + # android:java_resources are packed *_percent.pak and placed in the executable folder + deps += [ "//components/user_scripts/android:java_resources" ] } if (enable_vr) { @@ -513,6 +517,7 @@ android_library("chrome_java") { "//components/ukm/android:java", "//components/url_formatter/android:url_formatter_java", "//components/user_prefs/android:java", + "//components/user_scripts/android:java", "//components/variations/android:variations_java", "//components/version_info/android:version_constants_java", "//components/viz/common:common_java", diff --git a/chrome/android/java/res/xml/main_preferences.xml b/chrome/android/java/res/xml/main_preferences.xml --- a/chrome/android/java/res/xml/main_preferences.xml +++ b/chrome/android/java/res/xml/main_preferences.xml @@ -86,6 +86,11 @@ android:key="useragent_settings" android:order="20" android:title="@string/prefs_useragent_settings"/> + GetPrefs()->GetBoolean(prefs::kForceGoogleSafeSearch), profile->GetPrefs()->GetInteger(prefs::kForceYouTubeRestrict), - profile->GetPrefs()->GetString(prefs::kAllowedDomainsForApps)}; + profile->GetPrefs()->GetString(prefs::kAllowedDomainsForApps), + false /*-> allow_userscript, don't care */}; result.push_back(std::make_unique( #if defined(OS_ANDROID) client_data_header, is_tab_large_enough, diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc --- a/chrome/browser/flag_descriptions.cc +++ b/chrome/browser/flag_descriptions.cc @@ -5239,6 +5239,11 @@ const char kUserDataSnapshotDescription[] = "update and restoring them after a version rollback."; #endif // !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) +const char kEnableLoggingUserScriptsName[] = "Enable logging user scripts component"; +const char kEnableLoggingUserScriptsDescription[] = + "Enables logging for troubleshooting feature. " + "Enabling logs may make browsing slower."; + #if defined(OS_WIN) || BUILDFLAG(IS_CHROMEOS_ASH) || defined(OS_MAC) const char kWebShareName[] = "Web Share"; const char kWebShareDescription[] = diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h --- a/chrome/browser/flag_descriptions.h +++ b/chrome/browser/flag_descriptions.h @@ -3046,6 +3046,9 @@ extern const char kUserDataSnapshotName[]; extern const char kUserDataSnapshotDescription[]; #endif // !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) +extern const char kEnableLoggingUserScriptsName[]; +extern const char kEnableLoggingUserScriptsDescription[]; + #if defined(OS_WIN) || BUILDFLAG(IS_CHROMEOS_ASH) || defined(OS_MAC) extern const char kWebShareName[]; extern const char kWebShareDescription[]; diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc --- a/chrome/browser/prefs/browser_prefs.cc +++ b/chrome/browser/prefs/browser_prefs.cc @@ -233,6 +233,7 @@ #include "components/ntp_tiles/popular_sites_impl.h" #include "components/permissions/contexts/geolocation_permission_context_android.h" #include "components/query_tiles/tile_service_prefs.h" +#include "components/user_scripts/browser/user_script_prefs.h" #else // defined(OS_ANDROID) #include "chrome/browser/accessibility/live_caption_controller.h" #include "chrome/browser/cart/cart_service.h" @@ -1148,6 +1149,7 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry, translate::TranslatePrefs::RegisterProfilePrefs(registry); omnibox::RegisterProfilePrefs(registry); ZeroSuggestProvider::RegisterProfilePrefs(registry); + user_scripts::UserScriptsPrefs::RegisterProfilePrefs(registry); #if BUILDFLAG(ENABLE_SESSION_SERVICE) RegisterSessionServiceLogProfilePrefs(registry); diff --git a/chrome/browser/profiles/BUILD.gn b/chrome/browser/profiles/BUILD.gn --- a/chrome/browser/profiles/BUILD.gn +++ b/chrome/browser/profiles/BUILD.gn @@ -45,6 +45,7 @@ source_set("profile") { "//components/profile_metrics", "//components/sync/driver", "//components/variations", + "//components/user_scripts/browser", "//content/public/browser", "//extensions/buildflags", ] diff --git a/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc b/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc --- a/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc +++ b/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc @@ -212,6 +212,8 @@ #include "chrome/browser/ui/cocoa/screentime/screentime_features.h" #endif +#include "components/user_scripts/browser/userscripts_browser_client.h" + namespace chrome { void AddProfilesExtraParts(ChromeBrowserMainParts* main_parts) { @@ -503,6 +505,7 @@ void ChromeBrowserMainExtraPartsProfiles:: #endif WebDataServiceFactory::GetInstance(); webrtc_event_logging::WebRtcEventLogManagerKeyedServiceFactory::GetInstance(); + user_scripts::UserScriptsBrowserClient::GetInstance(); } void ChromeBrowserMainExtraPartsProfiles::PreProfileInit() { diff --git a/chrome/browser/profiles/profile_manager.cc b/chrome/browser/profiles/profile_manager.cc --- a/chrome/browser/profiles/profile_manager.cc +++ b/chrome/browser/profiles/profile_manager.cc @@ -113,6 +113,8 @@ #include "extensions/common/manifest.h" #endif +#include "components/user_scripts/browser/userscripts_browser_client.h" + #if BUILDFLAG(ENABLE_SESSION_SERVICE) #include "chrome/browser/sessions/session_service_factory.h" #endif @@ -1512,6 +1514,13 @@ void ProfileManager::DoFinalInitForServices(Profile* profile, } #endif + + user_scripts::UserScriptsBrowserClient* userscript_client = + user_scripts::UserScriptsBrowserClient::GetInstance(); + if (userscript_client) { + userscript_client->SetProfile(profile); + } + #if BUILDFLAG(ENABLE_SUPERVISED_USERS) // Initialization needs to happen after extension system initialization (for // extension::ManagementPolicy) and InitProfileUserPrefs (for setting the diff --git a/chrome/browser/profiles/renderer_updater.cc b/chrome/browser/profiles/renderer_updater.cc --- a/chrome/browser/profiles/renderer_updater.cc +++ b/chrome/browser/profiles/renderer_updater.cc @@ -29,6 +29,8 @@ #include "chrome/browser/ash/login/signin/oauth2_login_manager_factory.h" #endif +#include "components/user_scripts/browser/user_script_prefs.h" + namespace { #if BUILDFLAG(ENABLE_EXTENSIONS) @@ -75,6 +77,7 @@ RendererUpdater::RendererUpdater(Profile* profile) : profile_(profile) { force_google_safesearch_.Init(prefs::kForceGoogleSafeSearch, pref_service); force_youtube_restrict_.Init(prefs::kForceYouTubeRestrict, pref_service); allowed_domains_for_apps_.Init(prefs::kAllowedDomainsForApps, pref_service); + activate_userscripts_.Init(user_scripts::prefs::kUserScriptsEnabled, pref_service); pref_change_registrar_.Init(pref_service); pref_change_registrar_.Add( @@ -89,6 +92,10 @@ RendererUpdater::RendererUpdater(Profile* profile) : profile_(profile) { prefs::kAllowedDomainsForApps, base::BindRepeating(&RendererUpdater::UpdateAllRenderers, base::Unretained(this))); + pref_change_registrar_.Add( + user_scripts::prefs::kUserScriptsEnabled, + base::BindRepeating(&RendererUpdater::UpdateAllRenderers, + base::Unretained(this))); } RendererUpdater::~RendererUpdater() { @@ -223,5 +230,6 @@ void RendererUpdater::UpdateRenderer( ->SetConfiguration(chrome::mojom::DynamicParams::New( force_google_safesearch_.GetValue(), force_youtube_restrict_.GetValue(), - allowed_domains_for_apps_.GetValue())); + allowed_domains_for_apps_.GetValue(), + activate_userscripts_.GetValue())); } diff --git a/chrome/browser/profiles/renderer_updater.h b/chrome/browser/profiles/renderer_updater.h --- a/chrome/browser/profiles/renderer_updater.h +++ b/chrome/browser/profiles/renderer_updater.h @@ -82,6 +82,7 @@ class RendererUpdater : public KeyedService, // Prefs that we sync to the renderers. BooleanPrefMember force_google_safesearch_; + BooleanPrefMember activate_userscripts_; IntegerPrefMember force_youtube_restrict_; StringPrefMember allowed_domains_for_apps_; diff --git a/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc b/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc --- a/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc +++ b/chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc @@ -84,6 +84,7 @@ #include "components/security_interstitials/content/urls.h" #include "components/signin/public/base/signin_buildflags.h" #include "components/site_engagement/content/site_engagement_service.h" +#include "components/user_scripts/browser/ui/user_scripts_ui.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_ui.h" #include "content/public/common/content_client.h" @@ -650,6 +651,8 @@ WebUIFactoryFunction GetWebUIFactoryFunction(WebUI* web_ui, return &NewWebUI; if (url.host_piece() == chrome::kChromeUIVersionHost) return &NewWebUI; + if (url.host_piece() == user_scripts::kChromeUIUserScriptsHost) + return &NewWebUI; #if !defined(OS_ANDROID) #if !BUILDFLAG(IS_CHROMEOS_ASH) diff --git a/chrome/chrome_paks.gni b/chrome/chrome_paks.gni --- a/chrome/chrome_paks.gni +++ b/chrome/chrome_paks.gni @@ -105,6 +105,7 @@ template("chrome_extra_paks") { "$root_gen_dir/third_party/blink/public/resources/inspector_overlay_resources.pak", "$root_gen_dir/ui/resources/webui_generated_resources.pak", "$root_gen_dir/ui/resources/webui_resources.pak", + "$root_gen_dir/chrome/userscripts_browser_resources.pak", ] deps = [ "//base/tracing/protos:chrome_track_event_resources", @@ -120,6 +121,7 @@ template("chrome_extra_paks") { "//third_party/blink/public:devtools_inspector_resources", "//third_party/blink/public:resources", "//ui/resources", + "//components/user_scripts/browser:userscripts_browser_resources_grit", ] if (defined(invoker.deps)) { deps += invoker.deps diff --git a/chrome/common/renderer_configuration.mojom b/chrome/common/renderer_configuration.mojom --- a/chrome/common/renderer_configuration.mojom +++ b/chrome/common/renderer_configuration.mojom @@ -12,6 +12,7 @@ struct DynamicParams { bool force_safe_search = true; int32 youtube_restrict = 0; string allowed_domains_for_apps; + bool allow_userscript = false; }; interface ChromeOSListener { diff --git a/chrome/renderer/BUILD.gn b/chrome/renderer/BUILD.gn --- a/chrome/renderer/BUILD.gn +++ b/chrome/renderer/BUILD.gn @@ -171,6 +171,7 @@ static_library("renderer") { "//components/feed:feature_list", "//components/feed/content/renderer:feed_renderer", "//components/history_clusters/core", + "//components/user_scripts/renderer", "//components/network_hints/renderer", "//components/no_state_prefetch/common", "//components/no_state_prefetch/renderer", diff --git a/chrome/renderer/chrome_content_renderer_client.cc b/chrome/renderer/chrome_content_renderer_client.cc --- a/chrome/renderer/chrome_content_renderer_client.cc +++ b/chrome/renderer/chrome_content_renderer_client.cc @@ -251,6 +251,9 @@ #include "chrome/renderer/supervised_user/supervised_user_error_page_controller_delegate_impl.h" #endif +#include "components/user_scripts/common/user_scripts_features.h" +#include "components/user_scripts/renderer/user_scripts_renderer_client.h" + using autofill::AutofillAgent; using autofill::AutofillAssistantAgent; using autofill::PasswordAutofillAgent; @@ -405,6 +408,12 @@ void ChromeContentRendererClient::RenderThreadStarted() { WebString::FromASCII(extensions::kExtensionScheme)); #endif + user_scripts::UserScriptsRendererClient* userscript_client = + user_scripts::UserScriptsRendererClient::GetInstance(); + if (userscript_client) { + userscript_client->RenderThreadStarted(); + } + #if BUILDFLAG(ENABLE_SPELLCHECK) if (!spellcheck_) InitSpellCheck(); @@ -541,6 +550,13 @@ void ChromeContentRendererClient::RenderFrameCreated( render_frame, registry); #endif + user_scripts::UserScriptsRendererClient* userscript_client = + user_scripts::UserScriptsRendererClient::GetInstance(); + if (userscript_client) { + userscript_client->RenderFrameCreated( + render_frame, registry); + } + #if BUILDFLAG(ENABLE_PLUGINS) new PepperHelper(render_frame); #endif @@ -1533,7 +1549,14 @@ void ChromeContentRendererClient::RunScriptsAtDocumentStart( ChromeExtensionsRendererClient::GetInstance()->RunScriptsAtDocumentStart( render_frame); // |render_frame| might be dead by now. + static_assert(false, "Compiler error: extensions cannot be enabled with user scripts"); #endif + user_scripts::UserScriptsRendererClient* userscript_client = + user_scripts::UserScriptsRendererClient::GetInstance(); + if (userscript_client) { + userscript_client->RunScriptsAtDocumentStart( + render_frame); + } } void ChromeContentRendererClient::RunScriptsAtDocumentEnd( @@ -1542,7 +1565,14 @@ void ChromeContentRendererClient::RunScriptsAtDocumentEnd( ChromeExtensionsRendererClient::GetInstance()->RunScriptsAtDocumentEnd( render_frame); // |render_frame| might be dead by now. + static_assert(false, "Compiler error: extensions cannot be enabled with user scripts"); #endif + user_scripts::UserScriptsRendererClient* userscript_client = + user_scripts::UserScriptsRendererClient::GetInstance(); + if (userscript_client) { + userscript_client->RunScriptsAtDocumentEnd( + render_frame); + } } void ChromeContentRendererClient::RunScriptsAtDocumentIdle( @@ -1551,7 +1581,14 @@ void ChromeContentRendererClient::RunScriptsAtDocumentIdle( ChromeExtensionsRendererClient::GetInstance()->RunScriptsAtDocumentIdle( render_frame); // |render_frame| might be dead by now. + static_assert(false, "Compiler error: extensions cannot be enabled with user scripts"); #endif + user_scripts::UserScriptsRendererClient* userscript_client = + user_scripts::UserScriptsRendererClient::GetInstance(); + if (userscript_client) { + userscript_client->RunScriptsAtDocumentIdle( + render_frame); + } } void ChromeContentRendererClient:: diff --git a/chrome/renderer/chrome_render_thread_observer.cc b/chrome/renderer/chrome_render_thread_observer.cc --- a/chrome/renderer/chrome_render_thread_observer.cc +++ b/chrome/renderer/chrome_render_thread_observer.cc @@ -59,6 +59,8 @@ #include "third_party/blink/public/web/web_security_policy.h" #include "third_party/blink/public/web/web_view.h" +#include "components/user_scripts/renderer/user_scripts_renderer_client.h" + #if BUILDFLAG(ENABLE_EXTENSIONS) #include "chrome/renderer/extensions/extension_localization_peer.h" #endif @@ -255,6 +257,7 @@ void ChromeRenderThreadObserver::SetInitialConfiguration( void ChromeRenderThreadObserver::SetConfiguration( chrome::mojom::DynamicParamsPtr params) { *GetDynamicConfigParams() = std::move(*params); + user_scripts::UserScriptsRendererClient::GetInstance()->ConfigurationUpdated(); } void ChromeRenderThreadObserver::SetContentSettingRules( diff --git a/components/components_strings.grd b/components/components_strings.grd --- a/components/components_strings.grd +++ b/components/components_strings.grd @@ -334,6 +334,7 @@ + diff --git a/components/user_scripts/README.md b/components/user_scripts/README.md new file mode 100644 --- /dev/null +++ b/components/user_scripts/README.md @@ -0,0 +1,150 @@ +# Userscripts support for Bromite + +UserScript support is under user setting currently disabled by default: when disabled, no code that can impact navigation safety is active. + +Activation allows the use of userscripts in Bromite. It is possible to add them in two ways: +- by selecting files from the file picker in the settings +- downloading the scripts and opening it from downloads (only if ends with .user.js) +The new imported scripts are disabled by default: they can be activated via the menu visible on the ui. + +Userscript support is currently the one provided by the desktop version. The enabled headers are: + +- `@name` +- `@version` +- `@description` +- `@url` or `@homepage` +- `@include`, `@exclude`, `@match`, `@exclude_match` for the url pattern (only http e https) +- `@run-at` + - `document-start` + Start the script after the documentElement is created, but before anything else happens + - `document-end` + Start the script after the entire document is parsed. Same as DOMContentLoaded + - `document-idle` + Start the script sometime after DOMContentLoaded, as soon as the document is "idle". Currently this uses the simple heuristic of: min(DOM_CONTENT_LOADED + TIMEOUT, ONLOAD), but no particular injection point is guaranteed + +The url-patterns are so defined: +``` +// := :// | '' +// := '*' | 'http' | 'https' +// := '*' | | [] | +// '*.' + +// := [':' ('*' | )] +// := '/' +// +// * Host is not used when the scheme is 'file'. +// * The path can have embedded '*' characters which act as glob wildcards. +// * '' is a special pattern that matches any valid URL that contains +// a valid scheme (as specified by valid_schemes_). +// * The '*' scheme pattern excludes file URLs. +// +// Examples of valid patterns: +// - http://*/* +// - http://*/foo* +// - https://*.google.com/foo*bar +// - file://monkey* +// - http://127.0.0.1/* +// - http://[2607:f8b0:4005:805::200e]/* +// +// Examples of invalid patterns: +// - http://* -- path not specified +// - http://*foo/bar -- * not allowed as substring of host component +// - http://foo.*.bar/baz -- * must be first component +// - http:/bar -- scheme separator not found +// - foo://* -- invalid scheme +// - chrome:// -- we don't support chrome internal URLs +``` + +--- +## **Beware of the scripts you enter: they can be a source of security problems, you are injecting code into your navigation**. +--- +## Technical aspects + +`user_scripts/common` and `user_scripts/renderer` is the closest to the current chromium code: few changes there, mostly eliminated the superfluous extension related code. + +In `user_scripts/browser` you find the actual management (in the browser process) and in `android` basically the settings ui. + +At startup it tries to read all files in the `userscripts folder` in `/data/user/0/org.bromite.bromite/app_chrome/userscripts`: this could be a critical process because a crash would prevent the browser from opening, and that's why there is a crash counter that automatically disables the feature after three attempts if it encounters a problem during startup. + +The java ui allows userscript management: addition, deletion and activation/deactivation. Any errors while reading the scripts are presented to the user: scripts with errors cannot be activated. There is also the visualization of the script source and the open of its homepage (if foreseen) in incognito browsing. + +There is also support for an on-line help at https://github.com/bromite/bromite/wiki/UserScripts. + + +Entry points are `components/user_scripts/browser/userscripts_browser_client.cc` and `components/user_scripts/renderer/user_scripts_renderer_client.cc`: the two attach to the browser and the renderer process. + +for userscripts_browser_client.cc +- `chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc` +builds the browser side. also gpu process passes here, but the call is avoided. + +- `chrome/browser/profiles/profile_manager.cc` +set the profile + +for renderer/user_scripts_renderer_client.cc +- `chrome/renderer/chrome_content_renderer_client.cc` +at the renderer side + +the two sides have a life of their own and can communicate only via ipc, the renderer does not have access to the disk while the browser does only via its own task runner file (`components/user_scripts/browser/file_task_runner.cc`). + +## BROWSER PROCESS +Once the profile is set, it istance the `components/user_scripts/browser/user_script_loader.cc` and starts it. +This loads all the files in the folder into the runner file and interprets them. Control then passes to +`components/user_scripts/browser/user_script_prefs.cc` which verifies through the default profile what the user wants active. +At that point it passes through IPC to the renderer only the list of active scripts. + +## RENDERER PROCESS +Each time a frame is created, the script pattern is checked and it is injected into the three stages (START, IDLE, END). +The logic is all in `components/user_scripts/renderer/user_script_set.cc`. + +## Simple example +Here you find a working example that eliminates the google popup, useful in always incognito: +``` +// ==UserScript== +// @name Remove Google Consent +// @namespace google +// @version 0.0.1 +// @description Autohide Accepts Cookies +// @author uazo +// @match https://*.google.com/search?* +// @grant none +// @run-at document-start +// ==/UserScript== + +(function() { + 'use strict'; + + var prepareStyleSheet = function() { + var style = document.createElement('style'); + //style.setAttribute('media', 'screen'); + style.appendChild(document.createTextNode('')); + document.head.appendChild(style); + style.sheet.insertRule('body { overflow:scroll !important;position:unset !important }'); + }; + + var hideConsent = function() { + document.getElementById("lb").style.display = "none"; + }; + + var checkElementThenRun = function(selector, func) { + var el = document.querySelector(selector); + if ( el == null ) { + if (window.requestAnimationFrame != undefined) { + window.requestAnimationFrame(function(){ checkElementThenRun(selector, func)}); + } else { + document.addEventListener('readystatechange', function(e) { + if (document.readyState == 'complete') { + func(); + } + }); + } + } else { + func(); + } + } + + document.cookie = 'CONSENT=YES+IT.it+V13+BX;domain=.google.com'; + checkElementThenRun('head', prepareStyleSheet); + checkElementThenRun('#lb', hideConsent); +})(); +``` + +See also: https://github.com/bromite/bromite/pull/857 diff --git a/components/user_scripts/android/BUILD.gn b/components/user_scripts/android/BUILD.gn new file mode 100755 --- /dev/null +++ b/components/user_scripts/android/BUILD.gn @@ -0,0 +1,80 @@ +# This file is part of Bromite. + +# Bromite is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Bromite is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with Bromite. If not, see . + +import("//build/config/android/rules.gni") + +generate_jni("user_scripts_jni_headers") { + sources = [ "java/src/org/chromium/components/user_scripts/UserScriptsBridge.java" ] +} + +android_resources("java_resources") { + sources = [ + "java/res/xml/userscripts_preferences.xml", + "java/res/layout/accept_script_item.xml", + "java/res/layout/accept_script_list.xml", + "java/res/layout/scripts_preference.xml", + "java/res/values/dimens.xml" + ] + + deps = [ + "//components/browser_ui/strings/android:browser_ui_strings_grd", + "//components/browser_ui/styles/android:java_resources", + "//components/strings:components_strings_grd", + "//ui/android:ui_java_resources", + ] +} + +android_library("java") { + sources = [ + "java/src/org/chromium/components/user_scripts/FragmentWindowAndroid.java", + "java/src/org/chromium/components/user_scripts/UserScriptsPreferences.java", + "java/src/org/chromium/components/user_scripts/UserScriptsBridge.java", + "java/src/org/chromium/components/user_scripts/IUserScriptsUtils.java", + "java/src/org/chromium/components/user_scripts/ScriptListBaseAdapter.java", + "java/src/org/chromium/components/user_scripts/ScriptListPreference.java", + "java/src/org/chromium/components/user_scripts/ScriptInfo.java", + ] + deps = [ + ":java_resources", + "//base:base_java", + "//base:jni_java", + "//components/embedder_support/android:browser_context_java", + "//components/browser_ui/settings/android:java", + "//components/browser_ui/widget/android:java", + "//content/public/android:content_java", + "//components/prefs/android:java", + "//third_party/android_deps:android_support_v7_appcompat_java", + "//third_party/androidx:androidx_annotation_annotation_java", + "//third_party/androidx:androidx_appcompat_appcompat_resources_java", + "//third_party/androidx:androidx_preference_preference_java", + "//ui/android:ui_java", + ] + resources_package = "org.chromium.components.user_scripts" + annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] +} + +source_set("android") { + sources = [ + "user_scripts_bridge.cc", + "user_scripts_bridge.h", + ] + deps = [ + ":user_scripts_jni_headers", + "//base", + "//components/user_scripts/browser", + "//components/permissions", + "//content/public/browser", + ] +} diff --git a/components/user_scripts/android/java/res/layout/accept_script_item.xml b/components/user_scripts/android/java/res/layout/accept_script_item.xml new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java/res/layout/accept_script_item.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/user_scripts/android/java/res/layout/accept_script_list.xml b/components/user_scripts/android/java/res/layout/accept_script_list.xml new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java/res/layout/accept_script_list.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/components/user_scripts/android/java/res/layout/scripts_preference.xml b/components/user_scripts/android/java/res/layout/scripts_preference.xml new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java/res/layout/scripts_preference.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/components/user_scripts/android/java/res/values/dimens.xml b/components/user_scripts/android/java/res/values/dimens.xml new file mode 100755 --- /dev/null +++ b/components/user_scripts/android/java/res/values/dimens.xml @@ -0,0 +1,11 @@ + + + + + + 24dp + 260dp + + diff --git a/components/user_scripts/android/java/res/xml/userscripts_preferences.xml b/components/user_scripts/android/java/res/xml/userscripts_preferences.xml new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java/res/xml/userscripts_preferences.xml @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/components/user_scripts/android/java/src/org/chromium/chrome/browser/user_scripts/UserScriptsUtils.java b/components/user_scripts/android/java/src/org/chromium/chrome/browser/user_scripts/UserScriptsUtils.java new file mode 100755 --- /dev/null +++ b/components/user_scripts/android/java/src/org/chromium/chrome/browser/user_scripts/UserScriptsUtils.java @@ -0,0 +1,84 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +package org.chromium.chrome.browser.user_scripts; + +import android.content.Context; +import android.content.Intent; +import android.provider.Browser; +import android.provider.MediaStore; +import android.net.Uri; + +import org.chromium.base.ContextUtils; +import org.chromium.base.ContentUriUtils; +import org.chromium.base.IntentUtils; + +import org.chromium.chrome.browser.IntentHandler; +import org.chromium.chrome.browser.settings.SettingsLauncherImpl; +import org.chromium.components.browser_ui.settings.SettingsLauncher; + +import org.chromium.components.user_scripts.UserScriptsPreferences; +import org.chromium.components.user_scripts.UserScriptsBridge; +import org.chromium.components.user_scripts.IUserScriptsUtils; + +public class UserScriptsUtils implements IUserScriptsUtils +{ + private static UserScriptsUtils instance; + + private UserScriptsUtils() {} + + public static void Initialize() { + instance = new UserScriptsUtils(); + UserScriptsBridge.registerUtils(instance); + } + + public static UserScriptsUtils getInstance() { + return instance; + } + + public boolean openFile(String filePath, String mimeType, String downloadGuid, + String originalUrl, String referrer, Uri contentUri) { + if (UserScriptsBridge.isEnabled() == false) return false; + + Context context = ContextUtils.getApplicationContext(); + + if (ContentUriUtils.isContentUri(filePath)) + filePath = ContentUriUtils.getDisplayName(contentUri, context, + MediaStore.MediaColumns.DISPLAY_NAME); + + if (filePath.toUpperCase().endsWith(".USER.JS") == false) return false; + + SettingsLauncher settingsLauncher = new SettingsLauncherImpl(); + Intent intent = settingsLauncher.createSettingsActivityIntent( + context, UserScriptsPreferences.class.getName(), + UserScriptsPreferences.createFragmentArgsForInstall(filePath)); + IntentUtils.safeStartActivity(context, intent); + + return true; + } + + public void openSourceFile(String scriptKey) { + Context context = ContextUtils.getApplicationContext(); + + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("chrome://user-scripts/?key=" + scriptKey)); + intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); + intent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setPackage(context.getPackageName()); + IntentHandler.startChromeLauncherActivityForTrustedIntent(intent); + } +} \ No newline at end of file diff --git a/components/user_scripts/android/java/src/org/chromium/components/user_scripts/FragmentWindowAndroid.java b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/FragmentWindowAndroid.java new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/FragmentWindowAndroid.java @@ -0,0 +1,89 @@ +// Copyright 2019 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. + +package org.chromium.components.user_scripts; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Build; +import android.view.View; + +import androidx.fragment.app.Fragment; + +import org.chromium.ui.base.ActivityKeyboardVisibilityDelegate; +import org.chromium.ui.base.ActivityAndroidPermissionDelegate; +import org.chromium.ui.base.ImmutableWeakReference; +import org.chromium.ui.base.IntentRequestTracker; +import org.chromium.ui.base.IntentRequestTracker.Delegate; +import org.chromium.ui.base.WindowAndroid; + +import java.lang.ref.WeakReference; + +/** + * Implements intent sending for a fragment based window. This should be created when + * onAttach() is called on the fragment, and destroyed when onDetach() is called. + */ +public class FragmentWindowAndroid extends WindowAndroid { + private Fragment mFragment; + + private static class TrackerDelegateImpl implements Delegate { + private final Fragment mFragment; + // This WeakReference is purely to avoid gc churn of creating a new WeakReference in + // every getActivity call. It is not needed for correctness. + private ImmutableWeakReference mActivityWeakRefHolder; + + /** + * Create an instance of delegate for the given fragment that will own the + * IntentRequestTracker. + * @param fragment The fragment that owns the IntentRequestTracker. + */ + private TrackerDelegateImpl(Fragment fragment) { + mFragment = fragment; + } + + @Override + public boolean startActivityForResult(Intent intent, int requestCode) { + mFragment.startActivityForResult(intent, requestCode, null); + return true; + } + + @Override + public boolean startIntentSenderForResult(IntentSender intentSender, int requestCode) { + try { + mFragment.startIntentSenderForResult( + intentSender, requestCode, new Intent(), 0, 0, 0, null); + } catch (IntentSender.SendIntentException e) { + return false; + } + return true; + } + + @Override + public void finishActivity(int requestCode) { + Activity activity = getActivity().get(); + if (activity == null) return; + activity.finishActivity(requestCode); + } + + @Override + public final WeakReference getActivity() { + if (mActivityWeakRefHolder == null + || mActivityWeakRefHolder.get() != mFragment.getActivity()) { + mActivityWeakRefHolder = new ImmutableWeakReference<>(mFragment.getActivity()); + } + return mActivityWeakRefHolder; + } + } + + FragmentWindowAndroid(Context context, Fragment fragment) { + super(context, IntentRequestTracker.createFromDelegate(new TrackerDelegateImpl(fragment))); + mFragment = fragment; + + setKeyboardDelegate(new ActivityKeyboardVisibilityDelegate(getActivity())); + setAndroidPermissionDelegate(new ActivityAndroidPermissionDelegate(getActivity())); + } +} \ No newline at end of file diff --git a/components/user_scripts/android/java/src/org/chromium/components/user_scripts/IUserScriptsUtils.java b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/IUserScriptsUtils.java new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/IUserScriptsUtils.java @@ -0,0 +1,22 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +package org.chromium.components.user_scripts; + +public interface IUserScriptsUtils { + public void openSourceFile(String scriptKey); +} \ No newline at end of file diff --git a/components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptInfo.java b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptInfo.java new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptInfo.java @@ -0,0 +1,37 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +package org.chromium.components.user_scripts; + +import java.time.LocalDateTime; + +public class ScriptInfo { + public String Key; + public String Name; + public String Description; + public String Version; + public String FilePath; + public String UrlSource; + + public boolean Enabled; + public LocalDateTime InstallTime; + + public String ParserError; + public boolean ForceDisabled; + + public ScriptInfo() {} +} diff --git a/components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptListBaseAdapter.java b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptListBaseAdapter.java new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptListBaseAdapter.java @@ -0,0 +1,163 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +package org.chromium.components.user_scripts; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Switch; +import android.widget.CompoundButton; +import android.widget.LinearLayout; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import org.chromium.components.browser_ui.widget.dragreorder.DragReorderableListAdapter; +import org.chromium.components.browser_ui.widget.dragreorder.DragStateDelegate; +import org.chromium.components.browser_ui.widget.listmenu.ListMenuButton; +import org.chromium.components.browser_ui.widget.listmenu.ListMenuButtonDelegate; +import org.chromium.ui.widget.ChromeImageView; + +import java.util.ArrayList; +import java.util.List; + +public class ScriptListBaseAdapter extends DragReorderableListAdapter { + + class ItemClickListener { + void onScriptOnOff(boolean Enabled) {} + void onScriptClicked() {} + } + + static class ScriptInfoRowViewHolder extends ViewHolder { + private TextView mTitle; + private TextView mDescription; + private TextView mVersion; + private TextView mFile; + private TextView mUrl; + private TextView mError; + private Switch mSwitch; + private ChromeImageView mIcon; + private LinearLayout mErrorLayout; + private LinearLayout mUrlContainer; + + private ListMenuButton mMoreButton; + + private CompoundButton.OnCheckedChangeListener mOnOffListener; + + ScriptInfoRowViewHolder(View view) { + super(view); + + mSwitch = view.findViewById(R.id.switch_widget); + mTitle = view.findViewById(R.id.title); + mDescription = view.findViewById(R.id.description); + mVersion = view.findViewById(R.id.version); + mFile = view.findViewById(R.id.file); + mUrl = view.findViewById(R.id.url); + mUrlContainer = view.findViewById(R.id.url_container); + mError = view.findViewById(R.id.error); + mIcon = view.findViewById(R.id.icon); + mErrorLayout = view.findViewById(R.id.error_layout); + + mMoreButton = view.findViewById(R.id.more); + } + + protected void updateScriptInfo(ScriptInfo item) { + mSwitch.setOnCheckedChangeListener(null); + mSwitch.setChecked(item.Enabled); + mSwitch.setOnCheckedChangeListener(mOnOffListener); + + mSwitch.setEnabled(true); + if (item.ForceDisabled) { + mSwitch.setEnabled(false); + } + + mTitle.setText(item.Name); + mDescription.setText(item.Description); + mVersion.setText(item.Version); + mFile.setText(item.Key); + mUrl.setText(item.UrlSource); + mError.setText(item.ParserError); + + mUrl.setVisibility(View.VISIBLE); + if (item.UrlSource == null || item.UrlSource.isEmpty()) { + mUrlContainer.setVisibility(View.GONE); + } + mErrorLayout.setVisibility(View.VISIBLE); + if (item.ParserError == null || item.ParserError.isEmpty()) { + mErrorLayout.setVisibility(View.GONE); + } + } + + void setMenuButtonDelegate(@NonNull ListMenuButtonDelegate delegate) { + mMoreButton.setVisibility(View.VISIBLE); + mMoreButton.setDelegate(delegate); + // Set item row end padding 0 when MenuButton is visible. + ViewCompat.setPaddingRelative(itemView, ViewCompat.getPaddingStart(itemView), + itemView.getPaddingTop(), 0, itemView.getPaddingBottom()); + } + + void setItemListener(@NonNull ItemClickListener listener) { + mOnOffListener = (buttonView, isChecked) -> listener.onScriptOnOff(isChecked); + mSwitch.setOnCheckedChangeListener(mOnOffListener); + itemView.setOnClickListener(view -> listener.onScriptClicked()); + } + } + + ScriptListBaseAdapter(Context context) { + super(context); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { + View row = LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.accept_script_item, viewGroup, false); + return new ScriptInfoRowViewHolder(row); + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, int i) { + ((ScriptInfoRowViewHolder) viewHolder).updateScriptInfo(mElements.get(i)); + } + + void setDisplayedScriptInfo(List values) { + mElements = new ArrayList<>(values); + notifyDataSetChanged(); + } + + @Override + protected void setOrder(List order) { + } + + @Override + protected boolean isActivelyDraggable(ViewHolder viewHolder) { + return isPassivelyDraggable(viewHolder); + } + + @Override + protected boolean isPassivelyDraggable(ViewHolder viewHolder) { + return viewHolder instanceof ScriptInfoRowViewHolder; + } +} diff --git a/components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptListPreference.java b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptListPreference.java new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/ScriptListPreference.java @@ -0,0 +1,171 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +package org.chromium.components.user_scripts; + +import static org.chromium.components.browser_ui.widget.listmenu.BasicListMenu.buildMenuListItem; +import static org.chromium.components.browser_ui.widget.listmenu.BasicListMenu.buildMenuListItemWithEndIcon; + +import android.content.Context; +import android.content.Intent; +import android.provider.Browser; +import android.net.Uri; +import android.util.AttributeSet; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import org.chromium.ui.base.WindowAndroid; +import org.chromium.ui.base.ActivityWindowAndroid; + +import org.chromium.base.ApplicationStatus; +import org.chromium.base.ContextUtils; +import org.chromium.components.browser_ui.widget.TintedDrawable; +import org.chromium.components.browser_ui.widget.listmenu.BasicListMenu; +import org.chromium.components.browser_ui.widget.listmenu.ListMenu; +import org.chromium.components.browser_ui.widget.listmenu.ListMenuItemProperties; +import org.chromium.ui.modelutil.MVCListAdapter.ModelList; + +import org.chromium.components.user_scripts.ScriptListBaseAdapter; +import org.chromium.components.user_scripts.FragmentWindowAndroid; + +import java.util.List; + +public class ScriptListPreference extends Preference { + private static class ScriptListAdapter + extends ScriptListBaseAdapter { + private final Context mContext; + + ScriptListAdapter(Context context) { + super(context); + mContext = context; + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + super.onBindViewHolder(holder, position); + + final ScriptInfo info = getItemByPosition(position); + + ModelList menuItems = new ModelList(); + + menuItems.add(buildMenuListItem(R.string.remove, 0, 0, true)); + menuItems.add(buildMenuListItem(R.string.scripts_view_source, 0, 0, + info.ParserError == null || info.ParserError.isEmpty())); + + ListMenu.Delegate delegate = (model) -> { + int textId = model.get(ListMenuItemProperties.TITLE_ID); + if (textId == R.string.remove) { + UserScriptsBridge.RemoveScript(info.Key); + } else if (textId == R.string.scripts_view_source) { + UserScriptsBridge.getUtils().openSourceFile(info.Key); + } + }; + ((ScriptInfoRowViewHolder) holder) + .setMenuButtonDelegate(() -> new BasicListMenu(mContext, menuItems, delegate)); + ((ScriptInfoRowViewHolder) holder) + .setItemListener(new ScriptListBaseAdapter.ItemClickListener() { + @Override + public void onScriptOnOff(boolean Enabled) { + UserScriptsBridge.SetScriptEnabled(info.Key, Enabled); + } + + @Override + public void onScriptClicked() {} + }); + } + + // @Override + public void onDataUpdated(boolean enabled) { + List list = UserScriptsBridge.getUserScriptItems(); + if (enabled == false) { + for (ScriptInfo script : list) { + script.ForceDisabled = true; + } + } + setDisplayedScriptInfo(list); + } + } + + private TextView mAddButton; + private RecyclerView mRecyclerView; + private ScriptListAdapter mAdapter; + private FragmentWindowAndroid mWindowAndroid; + + public ScriptListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + mAdapter = new ScriptListAdapter(context); + } + + public void setWindowAndroid(FragmentWindowAndroid windowAndroid) { + mWindowAndroid = windowAndroid; + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + mAddButton = (TextView) holder.findViewById(R.id.add_script); + mAddButton.setCompoundDrawablesRelativeWithIntrinsicBounds( + TintedDrawable.constructTintedDrawable( + getContext(), R.drawable.plus, R.color.default_control_color_active), + null, null, null); + mAddButton.setOnClickListener(view -> { + UserScriptsBridge.SelectAndAddScriptFromFile(mWindowAndroid); + }); + + mRecyclerView = (RecyclerView) holder.findViewById(R.id.script_list); + LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + mRecyclerView.setLayoutManager(layoutManager); + mRecyclerView.addItemDecoration( + new DividerItemDecoration(getContext(), layoutManager.getOrientation())); + mRecyclerView.setEnabled(this.isEnabled()); + UserScriptsBridge.RegisterLoadCallback(this); + + // We do not want the RecyclerView to be announced by screen readers every time + // the view is bound. + if (mRecyclerView.getAdapter() != mAdapter) { + mRecyclerView.setAdapter(mAdapter); + // Initialize script list. + mAdapter.onDataUpdated(this.isEnabled()); + } + } + + @Override + public void setEnabled (boolean enabled) { + super.setEnabled(enabled); + if (mRecyclerView != null) mRecyclerView.setEnabled(enabled); + NotifyScriptsChanged(); + } + + public void NotifyScriptsChanged() { + mAdapter.onDataUpdated(this.isEnabled()); + } + + public void OnUserScriptLoaded(boolean result, String error) { + if (result == false) { + Toast toast = Toast.makeText(getContext(), error, Toast.LENGTH_LONG); + toast.show(); + } + } +} \ No newline at end of file diff --git a/components/user_scripts/android/java/src/org/chromium/components/user_scripts/UserScriptsBridge.java b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/UserScriptsBridge.java new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/UserScriptsBridge.java @@ -0,0 +1,200 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +package org.chromium.components.user_scripts; + +import java.util.ArrayList; +import java.util.List; +import java.lang.ref.WeakReference; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.Nullable; +import android.app.AlertDialog; +import android.content.DialogInterface; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.base.Log; +import org.chromium.ui.base.WindowAndroid; + +import org.chromium.components.user_scripts.ScriptListPreference; +import org.chromium.components.user_scripts.IUserScriptsUtils; + +@JNINamespace("user_scripts") +public class UserScriptsBridge { + private static final String TAG = "UserScript"; + + static WeakReference observer; + + private static IUserScriptsUtils utilInstance; + + public static void registerUtils(IUserScriptsUtils instance) { + utilInstance = instance; + } + + public static IUserScriptsUtils getUtils() { + return utilInstance; + } + + public static boolean isEnabled() { + return UserScriptsBridgeJni.get().isEnabled(); + } + + public static void setEnabled(boolean enabled) { + UserScriptsBridgeJni.get().setEnabled(enabled); + } + + public static void RemoveScript(String key) { + UserScriptsBridgeJni.get().removeScript(key); + } + + public static void SetScriptEnabled(String key, + boolean enabled) { + UserScriptsBridgeJni.get().setScriptEnabled(key, enabled); + } + + public static void Reload() { + UserScriptsBridgeJni.get().reload(); + } + + public static void SelectAndAddScriptFromFile(WindowAndroid window) { + Context context = window.getContext().get(); + + Intent fileSelector = new Intent(Intent.ACTION_OPEN_DOCUMENT); + fileSelector.addCategory(Intent.CATEGORY_OPENABLE); + fileSelector.setType("*/*"); + fileSelector.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + window.showIntent(fileSelector, + new WindowAndroid.IntentCallback() { + @Override + public void onIntentCompleted(WindowAndroid window, int resultCode, Intent data) { + if (data == null) return; + Uri filePath = data.getData(); + TryToInstall(context, filePath.toString()); + } + }, + null); + } + + public static void TryToInstall(Context context, String ScriptFullPath) { + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which){ + case DialogInterface.BUTTON_POSITIVE: + UserScriptsBridgeJni.get().tryToInstall(ScriptFullPath); + break; + + case DialogInterface.BUTTON_NEGATIVE: + break; + } + } + }; + + String message = context.getString(R.string.ask_to_install, ScriptFullPath); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setMessage(message) + .setPositiveButton(context.getString(R.string.yes), dialogClickListener) + .setNegativeButton(context.getString(R.string.no), dialogClickListener) + .show(); + } + + public static List getUserScriptItems() { + List list = new ArrayList<>(); + try { + String json = UserScriptsBridgeJni.get().getScriptsInfo(); + + JSONObject jsonObject = new JSONObject(json); + + JSONArray scripts = jsonObject.names(); + if (scripts != null) { + Log.i(TAG, "User Scripts Loaded: %s", json); + Log.i(TAG, "Totals scripts: %s", Integer.toString(scripts.length())); + for (int i = 0; i < scripts.length(); i++) { + String key = (String) scripts.get(i); + JSONObject script = jsonObject.getJSONObject(key); + + ScriptInfo si = new ScriptInfo(); + si.Key = key; + list.add(si); + + if(script.has("name")) si.Name = script.getString("name"); + if(script.has("description")) si.Description = script.getString("description"); + if(script.has("version")) si.Version = script.getString("version"); + if(script.has("file_path")) si.FilePath = script.getString("file_path"); + if(script.has("url_source")) si.UrlSource = script.getString("url_source"); + if(script.has("parser_error")) si.ParserError = script.getString("parser_error"); + if(script.has("force_disabled")) si.ForceDisabled = script.getBoolean("force_disabled");; + si.Enabled = script.getBoolean("enabled"); + } + } else { + Log.i(TAG, "User Scripts list empty"); + } + } catch (Exception e) { + Log.e(TAG, "User Scripts Load Error", e.toString()); + } + return list; + } + + public static void RegisterLoadCallback(ScriptListPreference caller) { + UserScriptsBridgeJni.get().registerLoadCallback(); + observer = new WeakReference(caller); + } + + @CalledByNative + private static void shouldRefreshUserScriptList() { + ScriptListPreference reference = observer.get(); + if (reference != null) { + reference.NotifyScriptsChanged(); + } + } + + @CalledByNative + private static void onUserScriptLoaded(boolean result, String error) { + ScriptListPreference reference = observer.get(); + if (reference != null) { + reference.OnUserScriptLoaded(result, error); + } + } + + @NativeMethods + interface Natives { + boolean isEnabled(); + void setEnabled(boolean enabled); + + String getScriptsInfo(); + + void removeScript(String scriptKey); + void setScriptEnabled(String scriptKey, boolean enabled); + + void reload(); + void selectAndAddScriptFromFile(WindowAndroid window); + void tryToInstall(String scriptFullPath); + + void registerLoadCallback(); + } + +} diff --git a/components/user_scripts/android/java/src/org/chromium/components/user_scripts/UserScriptsPreferences.java b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/UserScriptsPreferences.java new file mode 100755 --- /dev/null +++ b/components/user_scripts/android/java/src/org/chromium/components/user_scripts/UserScriptsPreferences.java @@ -0,0 +1,116 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +package org.chromium.components.user_scripts; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Browser; +import android.net.Uri; +import android.view.MenuItem; +import android.view.View; + +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +import org.chromium.base.Log; +import org.chromium.ui.base.WindowAndroid; +import org.chromium.ui.base.ActivityWindowAndroid; + +import org.chromium.components.browser_ui.settings.ChromeSwitchPreference; +import org.chromium.components.browser_ui.settings.SettingsUtils; +import org.chromium.components.browser_ui.settings.TextMessagePreference; + +import org.chromium.components.user_scripts.UserScriptsBridge; +import org.chromium.components.user_scripts.FragmentWindowAndroid; + +public class UserScriptsPreferences + extends PreferenceFragmentCompat + implements SettingsUtils.ISupportHelpAndFeedback { + + private static final String PREF_ENABLED_SWITCH = "enabled_switch"; + private static final String PREF_SCRIPTLISTPREFERENCE = "script_list"; + + public static final String EXTRA_SCRIPT_FILE = "org.chromium.chrome.preferences.script_file"; + + private FragmentWindowAndroid mWindowAndroid; + + @Override + public void onDestroy() { + if (mWindowAndroid != null) mWindowAndroid.destroy(); + super.onDestroy(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + getActivity().setTitle(R.string.prefs_userscripts_settings); + SettingsUtils.addPreferencesFromResource(this, R.xml.userscripts_preferences); + + ChromeSwitchPreference enabledSwitch = + (ChromeSwitchPreference) findPreference(PREF_ENABLED_SWITCH); + ScriptListPreference listPreference = + (ScriptListPreference) findPreference(PREF_SCRIPTLISTPREFERENCE); + + boolean enabled = UserScriptsBridge.isEnabled(); + enabledSwitch.setChecked(enabled); + listPreference.setEnabled(enabled); + enabledSwitch.setOnPreferenceChangeListener((preference, newValue) -> { + UserScriptsBridge.setEnabled((boolean) newValue); + listPreference.setEnabled((boolean) newValue); + return true; + }); + + mWindowAndroid = new FragmentWindowAndroid(getContext(), this); + listPreference.setWindowAndroid(mWindowAndroid); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + // handle picker callback from SelectFileDialog + mWindowAndroid.getIntentRequestTracker().onActivityResult(requestCode, resultCode, data, mWindowAndroid); + } + + public void onHelpAndFeebackPressed() { + Context context = getContext(); + + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/bromite/bromite/wiki/UserScripts")); + // Let Chromium know that this intent is from Chromium, so that it does not close the app when + // the user presses 'back' button. + intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); + intent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true); + intent.setPackage(context.getPackageName()); + context.startActivity(intent); + } + + public static Bundle createFragmentArgsForInstall(String filePath) { + Bundle fragmentArgs = new Bundle(); + fragmentArgs.putSerializable(EXTRA_SCRIPT_FILE, filePath); + return fragmentArgs; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + String scriptToInstall = (String)getArguments().getSerializable(EXTRA_SCRIPT_FILE); + if (scriptToInstall != null) { + UserScriptsBridge.TryToInstall(getContext(), scriptToInstall); + } + } +} diff --git a/components/user_scripts/android/java_sources.gni b/components/user_scripts/android/java_sources.gni new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/java_sources.gni @@ -0,0 +1,18 @@ +# This file is part of Bromite. + +# Bromite is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Bromite is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with Bromite. If not, see . + +userscripts_java_sources = [ + "//components/user_scripts/android/java/src/org/chromium/chrome/browser/user_scripts/UserScriptsUtils.java", +] diff --git a/components/user_scripts/android/user_scripts_bridge.cc b/components/user_scripts/android/user_scripts_bridge.cc new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/user_scripts_bridge.cc @@ -0,0 +1,173 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#include +#include +#include +#include +#include +#include + +#include "base/android/callback_android.h" +#include "base/android/jni_android.h" +#include "base/android/jni_array.h" +#include "base/android/jni_string.h" +#include "base/android/scoped_java_ref.h" +#include "ui/android/window_android.h" + +#include "components/user_scripts/android/user_scripts_jni_headers/UserScriptsBridge_jni.h" +#include "../browser/userscripts_browser_client.h" +#include "user_scripts_bridge.h" + +using base::android::AttachCurrentThread; +using base::android::ConvertJavaStringToUTF8; +using base::android::ConvertUTF16ToJavaString; +using base::android::ConvertUTF8ToJavaString; +using base::android::JavaParamRef; +using base::android::JavaRef; +using base::android::ScopedJavaGlobalRef; +using base::android::ScopedJavaLocalRef; +using content::BrowserContext; + +namespace { + +user_scripts::UserScriptsBrowserClient* GetUserScriptsBrowserClient() { + return user_scripts::UserScriptsBrowserClient::GetInstance(); +} + +class CallbackObserver : public user_scripts::UserScriptLoader::Observer { + private: + void OnScriptsLoaded(user_scripts::UserScriptLoader* loader, + content::BrowserContext* browser_context) override { + user_scripts::ShouldRefreshUserScriptList(base::android::AttachCurrentThread()); + } + + void OnUserScriptLoaded(user_scripts::UserScriptLoader* loader, + bool result, const std::string& error) override { + user_scripts::OnUserScriptLoaded(base::android::AttachCurrentThread(), + result, error); + } + + void OnUserScriptLoaderDestroyed(user_scripts::UserScriptLoader* loader) override {} +}; + +CallbackObserver* g_userscripts_loader_observer = NULL; + +} + +namespace user_scripts { + +static jboolean JNI_UserScriptsBridge_IsEnabled( + JNIEnv* env) { + user_scripts::UserScriptsBrowserClient* client = GetUserScriptsBrowserClient(); + if (client == NULL) return false; + return client->GetPrefs()->IsEnabled(); +} + +static void JNI_UserScriptsBridge_SetEnabled( + JNIEnv* env, + jboolean is_enabled) { + user_scripts::UserScriptsBrowserClient* client = GetUserScriptsBrowserClient(); + if (client == NULL) return; + client->GetPrefs()->SetEnabled(is_enabled); + client->GetLoader()->StartLoad(); +} + +static base::android::ScopedJavaLocalRef JNI_UserScriptsBridge_GetScriptsInfo( + JNIEnv* env) { + user_scripts::UserScriptsBrowserClient* client = GetUserScriptsBrowserClient(); + if (client == NULL) return ConvertUTF8ToJavaString(env, {}); + + std::string json = client->GetPrefs()->GetScriptsInfo(); + return ConvertUTF8ToJavaString(env, json); +} + +static void JNI_UserScriptsBridge_RemoveScript( + JNIEnv* env, + const JavaParamRef& jscript_key) { + user_scripts::UserScriptsBrowserClient* client = GetUserScriptsBrowserClient(); + if (client == NULL) return; + + std::string script_key = base::android::ConvertJavaStringToUTF8(jscript_key); + client->GetLoader()->RemoveScript(script_key); +} + +static void JNI_UserScriptsBridge_SetScriptEnabled( + JNIEnv* env, + const JavaParamRef& jscript_key, + jboolean is_enabled) { + user_scripts::UserScriptsBrowserClient* client = GetUserScriptsBrowserClient(); + if (client == NULL) return; + + std::string script_key = base::android::ConvertJavaStringToUTF8(jscript_key); + client->GetLoader()->SetScriptEnabled(script_key, is_enabled); +} + +static void JNI_UserScriptsBridge_Reload( + JNIEnv* env) { + user_scripts::UserScriptsBrowserClient* client = GetUserScriptsBrowserClient(); + if (client == NULL) return; + + client->GetLoader()->StartLoad(); + user_scripts::ShouldRefreshUserScriptList(env); +} + +static void JNI_UserScriptsBridge_SelectAndAddScriptFromFile( + JNIEnv* env, + const JavaParamRef& jwindow_android) { + user_scripts::UserScriptsBrowserClient* client = GetUserScriptsBrowserClient(); + if (client == NULL) return; + + client->GetLoader()->SelectAndAddScriptFromFile( + ui::WindowAndroid::FromJavaWindowAndroid(jwindow_android)); +} + +static void JNI_UserScriptsBridge_TryToInstall(JNIEnv* env, + const JavaParamRef& jscript_path) { + user_scripts::UserScriptsBrowserClient* client = GetUserScriptsBrowserClient(); + if (client == NULL) return; + + std::string script_path = base::android::ConvertJavaStringToUTF8(jscript_path); + base::FilePath path(script_path); + + client->GetLoader()->TryToInstall(path); +} + +static void JNI_UserScriptsBridge_RegisterLoadCallback( + JNIEnv* env) { + user_scripts::UserScriptsBrowserClient* client = GetUserScriptsBrowserClient(); + if (client == NULL) return; + + if (g_userscripts_loader_observer == NULL) { + g_userscripts_loader_observer = new CallbackObserver(); + client->GetLoader()->AddObserver(g_userscripts_loader_observer); + } +} + +static void ShouldRefreshUserScriptList(JNIEnv* env) { + Java_UserScriptsBridge_shouldRefreshUserScriptList(env); +} + +static void OnUserScriptLoaded(JNIEnv* env, + bool result, const std::string& error) { + base::android::ScopedJavaLocalRef j_error = + base::android::ConvertUTF8ToJavaString(env, error); + + Java_UserScriptsBridge_onUserScriptLoaded(env, result, j_error); +} + +} diff --git a/components/user_scripts/android/user_scripts_bridge.h b/components/user_scripts/android/user_scripts_bridge.h new file mode 100644 --- /dev/null +++ b/components/user_scripts/android/user_scripts_bridge.h @@ -0,0 +1,31 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#ifndef COMPONENTS_USERSCRIPS_HELPER_ANDROID_H_ +#define COMPONENTS_USERSCRIPS_HELPER_ANDROID_H_ + +#include + +namespace user_scripts { + +static void ShouldRefreshUserScriptList(JNIEnv* env); +static void OnUserScriptLoaded(JNIEnv* env, + bool result, const std::string& error); + +} // namespace user_scripts + +#endif \ No newline at end of file diff --git a/components/user_scripts/browser/BUILD.gn b/components/user_scripts/browser/BUILD.gn new file mode 100755 --- /dev/null +++ b/components/user_scripts/browser/BUILD.gn @@ -0,0 +1,83 @@ +# This file is part of Bromite. + +# Bromite is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Bromite is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with Bromite. If not, see . + +import("//build/config/features.gni") +import("//tools/grit/grit_rule.gni") + +group("browser") { + public_deps = [ + "//components/user_scripts/browser:browser_sources", + ] +} + +source_set("browser_sources") { + visibility = [ "./*" ] + + sources = [ + "file_task_runner.cc", + "file_task_runner.h", + "userscripts_browser_client.cc", + "userscripts_browser_client.h", + "user_script_loader.cc", + "user_script_loader.h", + "user_script_prefs.cc", + "user_script_prefs.h", + "user_script_pref_info.cc", + "user_script_pref_info.h", + "ui/user_scripts_ui.h", + "ui/user_scripts_ui.cc", + ] + + deps = [ + ":userscripts_browser_resources", + "//base:i18n", + "//components/keyed_service/content", + "//components/keyed_service/core", + "//components/pref_registry", + "//components/prefs", + "//content/public/browser", + "//crypto:platform", + "//components/user_scripts/common", + "//services/device/public/mojom", + "//services/preferences/public/cpp", + "//services/service_manager/public/cpp", + "//third_party/blink/public/common", + "//third_party/blink/public/mojom/frame", + "//ui/display", + ] + + public_deps = [ + "//content/public/common", + ] + + configs += [ + "//build/config:precompiled_headers", + "//build/config/compiler:wexit_time_destructors", + ] +} + +group("closure_compile") { + deps = [ "resources/user-script-ui:closure_compile" ] +} + +grit("userscripts_browser_resources") { + source = "resources/browser_resources.grd" + + output_dir = "$root_gen_dir/chrome" + outputs = [ + "grit/userscripts_browser_resources.h", + "userscripts_browser_resources.pak", + ] +} diff --git a/components/user_scripts/browser/file_task_runner.cc b/components/user_scripts/browser/file_task_runner.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/browser/file_task_runner.cc @@ -0,0 +1,40 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#include "file_task_runner.h" + +#include "base/sequenced_task_runner.h" +#include "base/task/lazy_thread_pool_task_runner.h" +#include "base/task/task_traits.h" + +namespace user_scripts { + +namespace { + +base::LazyThreadPoolSequencedTaskRunner g_us_task_runner = + LAZY_THREAD_POOL_SEQUENCED_TASK_RUNNER_INITIALIZER( + base::TaskTraits(base::MayBlock(), + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN, + base::TaskPriority::USER_VISIBLE)); + +} // namespace + +scoped_refptr GetUserScriptsFileTaskRunner() { + return g_us_task_runner.Get(); +} + +} // namespace user_scripts diff --git a/components/user_scripts/browser/file_task_runner.h b/components/user_scripts/browser/file_task_runner.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/browser/file_task_runner.h @@ -0,0 +1,34 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#ifndef USERSCRIPTS_BROWSER_FILE_TASK_RUNNER_H_ +#define USERSCRIPTS_BROWSER_FILE_TASK_RUNNER_H_ + +#include "base/memory/ref_counted.h" +#include "base/task/task_traits.h" + +namespace base { +class SequencedTaskRunner; +} + +namespace user_scripts { + +scoped_refptr GetUserScriptsFileTaskRunner(); + +} // namespace extensions + +#endif // USERSCRIPTS_BROWSER_FILE_TASK_RUNNER_H_ diff --git a/components/user_scripts/browser/resources/browser_resources.grd b/components/user_scripts/browser/resources/browser_resources.grd new file mode 100644 --- /dev/null +++ b/components/user_scripts/browser/resources/browser_resources.grd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/user_scripts/browser/resources/user-script-ui/BUILD.gn b/components/user_scripts/browser/resources/user-script-ui/BUILD.gn new file mode 100644 --- /dev/null +++ b/components/user_scripts/browser/resources/user-script-ui/BUILD.gn @@ -0,0 +1,12 @@ +import("//third_party/closure_compiler/compile_js.gni") + +js_type_check("closure_compile") { + deps = [ ":view_script_source" ] +} + +js_library("view_script_source") { + deps = [ + "//ui/webui/resources/js:cr.m", + "//ui/webui/resources/js:util.m", + ] +} \ No newline at end of file diff --git a/components/user_scripts/browser/resources/user-script-ui/user-scripts-ui.html b/components/user_scripts/browser/resources/user-script-ui/user-scripts-ui.html new file mode 100644 --- /dev/null +++ b/components/user_scripts/browser/resources/user-script-ui/user-scripts-ui.html @@ -0,0 +1,14 @@ + + + + + Local State Debug Page + + + + +
+    Loading Script Source file...
+  
+ + \ No newline at end of file diff --git a/components/user_scripts/browser/resources/user-script-ui/user-scripts-ui.js b/components/user_scripts/browser/resources/user-script-ui/user-scripts-ui.js new file mode 100644 --- /dev/null +++ b/components/user_scripts/browser/resources/user-script-ui/user-scripts-ui.js @@ -0,0 +1,9 @@ +import {sendWithPromise} from 'chrome://resources/js/cr.m.js'; +import {$} from 'chrome://resources/js/util.m.js'; + +document.addEventListener('DOMContentLoaded', function() { + const urlParams = new URLSearchParams(window.location.search); + sendWithPromise('requestSource', urlParams.get('key')).then(textContent => { + $('content').textContent = textContent[0].content; + }); +}); \ No newline at end of file diff --git a/components/user_scripts/browser/ui/user_scripts_ui.cc b/components/user_scripts/browser/ui/user_scripts_ui.cc new file mode 100644 --- /dev/null +++ b/components/user_scripts/browser/ui/user_scripts_ui.cc @@ -0,0 +1,148 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + + +#include + +#include "base/bind.h" +#include "base/json/json_string_value_serializer.h" +#include "base/macros.h" +#include "base/memory/writable_shared_memory_region.h" +#include "base/strings/string_util.h" +#include "base/values.h" + +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/url_constants.h" +#include "chrome/grit/userscripts_browser_resources.h" +#include "components/prefs/pref_service.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_controller.h" +#include "content/public/browser/web_ui_data_source.h" +#include "content/public/browser/web_ui_message_handler.h" + +#include "user_scripts_ui.h" +#include "../userscripts_browser_client.h" +#include "../../common/user_script.h" + +namespace { + +class UserScriptsUIHandler : public content::WebUIMessageHandler { + public: + UserScriptsUIHandler(); + ~UserScriptsUIHandler() override; + + // content::WebUIMessageHandler: + void RegisterMessages() override; + + private: + void HandleRequestSource(const base::ListValue* args); + void OnScriptsLoaded( + const std::string callback_id, + const std::string script_key, + std::unique_ptr user_scripts); + + std::unique_ptr loaded_scripts_; + + base::WeakPtrFactory weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(UserScriptsUIHandler); +}; + +UserScriptsUIHandler::UserScriptsUIHandler() + : loaded_scripts_(new user_scripts::UserScriptList()) { +} + +UserScriptsUIHandler::~UserScriptsUIHandler() { +} + +void UserScriptsUIHandler::RegisterMessages() { + web_ui()->RegisterMessageCallback( + "requestSource", + base::BindRepeating(&UserScriptsUIHandler::HandleRequestSource, + base::Unretained(this))); +} + +void UserScriptsUIHandler::HandleRequestSource(const base::ListValue* args) { + AllowJavascript(); + std::string callback_id; + CHECK(args->GetString(0, &callback_id)); + + std::string script_key; + if (args->GetString(1, &script_key) == false) { + std::string json = "Missing key value."; + ResolveJavascriptCallback(base::Value(callback_id), base::Value(json)); + return; + } + + user_scripts::UserScriptsBrowserClient* client = user_scripts::UserScriptsBrowserClient::GetInstance(); + if (client == NULL) { + std::string json = "User scripts disabled."; + ResolveJavascriptCallback(base::Value(callback_id), base::Value(json)); + } else { + std::unique_ptr scripts_to_load = + std::move(loaded_scripts_); + scripts_to_load->clear(); + + client->GetLoader()->LoadScripts(std::move(scripts_to_load), + base::BindOnce( + &UserScriptsUIHandler::OnScriptsLoaded, + weak_factory_.GetWeakPtr(), + callback_id, script_key) + ); + } +} + +void UserScriptsUIHandler::OnScriptsLoaded( + const std::string callback_id, + const std::string script_key, + std::unique_ptr user_scripts) { + loaded_scripts_ = std::move(user_scripts); + + base::ListValue response; + for (const std::unique_ptr& script : *loaded_scripts_) { + if (script->key() == script_key) { + auto scriptData = std::make_unique(); + for (const std::unique_ptr& js_file : + script->js_scripts()) { + base::StringPiece contents = js_file->GetContent(); + scriptData->SetString("content", contents.data()); + } + response.Append(std::move(scriptData)); + } + } + + ResolveJavascriptCallback(base::Value(callback_id), response); +} + +} // namespace + +namespace user_scripts { + +UserScriptsUI::UserScriptsUI(content::WebUI* web_ui) : WebUIController(web_ui) { + content::WebUIDataSource* html_source = + content::WebUIDataSource::Create(kChromeUIUserScriptsHost); + html_source->SetDefaultResource(IDR_USER_SCRIPTS_HTML); + html_source->AddResourcePath("user-scripts-ui.js", IDR_USER_SCRIPTS_JS); + content::WebUIDataSource::Add(Profile::FromWebUI(web_ui), html_source); + web_ui->AddMessageHandler(std::make_unique()); +} + +UserScriptsUI::~UserScriptsUI() { +} + +} \ No newline at end of file diff --git a/components/user_scripts/browser/ui/user_scripts_ui.h b/components/user_scripts/browser/ui/user_scripts_ui.h new file mode 100644 --- /dev/null +++ b/components/user_scripts/browser/ui/user_scripts_ui.h @@ -0,0 +1,39 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#ifndef USERSCRIPTS_BROWSER_UI_USER_SCRIPTS_UI_H_ +#define USERSCRIPTS_BROWSER_UI_USER_SCRIPTS_UI_H_ + +#include "base/macros.h" +#include "content/public/browser/web_ui_controller.h" + +namespace user_scripts { + +const char kChromeUIUserScriptsHost[] = "user-scripts"; + +class UserScriptsUI : public content::WebUIController { + public: + explicit UserScriptsUI(content::WebUI* web_ui); + ~UserScriptsUI() override; + + private: + DISALLOW_COPY_AND_ASSIGN(UserScriptsUI); +}; + +} + +#endif \ No newline at end of file diff --git a/components/user_scripts/browser/user_script_loader.cc b/components/user_scripts/browser/user_script_loader.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/browser/user_script_loader.cc @@ -0,0 +1,721 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#include "user_script_loader.h" + +#include + +#include +#include +#include + +#include "base/bind.h" +#include "base/memory/writable_shared_memory_region.h" +#include "base/strings/string_util.h" +#include "base/strings/strcat.h" +#include "base/files/file.h" +#include "base/files/file_util.h" +#include "base/files/file_enumerator.h" +#include "base/i18n/file_util_icu.h" +#include "base/path_service.h" +#include "base/base_paths_android.h" +#include "base/strings/utf_string_conversions.h" +#include "base/android/content_uri_utils.h" +#include "base/android/jni_android.h" +#include "base/task/task_traits.h" +#include "base/version.h" + +#include "crypto/sha2.h" +#include "base/base64.h" + +#include "build/build_config.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/render_process_host.h" +#include "ui/shell_dialogs/select_file_dialog.h" +#include "content/browser/file_system_access/file_system_chooser.h" +#include "chrome/browser/ui/chrome_select_file_policy.h" +#include "ui/android/window_android.h" + +#include "../common/user_scripts_features.h" +#include "../common/extension_messages.h" +#include "file_task_runner.h" +#include "user_script_prefs.h" +#include "user_script_pref_info.h" + +using content::BrowserThread; +using content::BrowserContext; + +namespace user_scripts { + +namespace { + +static const base::StringPiece kUserScriptBegin("// ==UserScript=="); +static const base::StringPiece kUserScriptEnd("// ==/UserScript=="); +static const base::StringPiece kNamespaceDeclaration("// @namespace"); +static const base::StringPiece kNameDeclaration("// @name"); +static const base::StringPiece kVersionDeclaration("// @version"); +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 kExcludeMatchDeclaration("// @exclude_match"); +static const base::StringPiece kRunAtDeclaration("// @run-at"); +static const base::StringPiece kRunAtDocumentStartValue("document-start"); +static const base::StringPiece kRunAtDocumentEndValue("document-end"); +static const base::StringPiece kRunAtDocumentIdleValue("document-idle"); +static const base::StringPiece kUrlSourceDeclaration("// @url"); +static const base::StringPiece kUrlHomePageDeclaration("// @homepage"); + +// internal use +static const base::StringPiece kParserError("// @error"); +static const base::StringPiece kForceDisabled("// @disabled"); + +bool invalidChar(unsigned char c) +{ + return !(c>=0 && c <128); +} + +void stripUnicode(std::string& str) +{ + str.erase(remove_if(str.begin(),str.end(), invalidChar), str.end()); +} + +// Helper function to parse greasesmonkey headers +bool GetDeclarationValue(const base::StringPiece& line, + const base::StringPiece& prefix, + std::string* value) { + base::StringPiece::size_type index = line.find(prefix); + if (index == base::StringPiece::npos) + return false; + + std::string temp(line.data() + index + prefix.length(), + line.length() - index - prefix.length()); + + if (temp.empty() || !base::IsUnicodeWhitespace(temp[0])) + return false; + + base::TrimWhitespaceASCII(temp, base::TRIM_ALL, value); + return true; +} + +} // namespace + +// static +bool UserScriptLoader::ParseMetadataHeader(const base::StringPiece& script_text, + std::unique_ptr& script, + bool *found_metadata, + std::string& error_message) { + // http://wiki.greasespot.net/Metadata_block + base::StringPiece line; + size_t line_start = 0; + size_t line_end = line_start; + *found_metadata = false; + + while (line_start < script_text.length()) { + line_end = script_text.find('\n', line_start); + + // Handle the case where there is no trailing newline in the file. + if (line_end == std::string::npos) + line_end = script_text.length() - 1; + + line = base::StringPiece(script_text.data() + line_start, + line_end - line_start); + + if (!*found_metadata) { + if (base::StartsWith(line, kUserScriptBegin)) + *found_metadata = true; + } else { + if (base::StartsWith(line, kUserScriptEnd)) + break; + + std::string value; + if (GetDeclarationValue(line, kIncludeDeclaration, &value)) { + // We escape some characters that MatchPattern() considers special. + base::ReplaceSubstringsAfterOffset(&value, 0, "\\", "\\\\"); + base::ReplaceSubstringsAfterOffset(&value, 0, "?", "\\?"); + script->add_glob(value); + } else if (GetDeclarationValue(line, kExcludeDeclaration, &value)) { + base::ReplaceSubstringsAfterOffset(&value, 0, "\\", "\\\\"); + base::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, kVersionDeclaration, &value)) { + base::Version version(value); + if (version.IsValid()) + script->set_version(version.GetString()); + } else if (GetDeclarationValue(line, kDescriptionDeclaration, &value)) { + script->set_description(value); + } else if (GetDeclarationValue(line, kMatchDeclaration, &value)) { + URLPattern pattern(UserScript::ValidUserScriptSchemes()); + if (URLPattern::ParseResult::kSuccess != pattern.Parse(value)) { + error_message = "Invalid UserScript Schema " + value; + return false; + } + script->add_url_pattern(pattern); + } else if (GetDeclarationValue(line, kExcludeMatchDeclaration, &value)) { + URLPattern exclude(UserScript::ValidUserScriptSchemes()); + if (URLPattern::ParseResult::kSuccess != exclude.Parse(value)) { + error_message = "Invalid UserScript Schema " + value; + return false; + } + script->add_exclude_url_pattern(exclude); + } else if (GetDeclarationValue(line, kRunAtDeclaration, &value)) { + if (value == kRunAtDocumentStartValue) + script->set_run_location(UserScript::DOCUMENT_START); + else if (value == kRunAtDocumentEndValue) + script->set_run_location(UserScript::DOCUMENT_END); + else if (value == kRunAtDocumentIdleValue) + script->set_run_location(UserScript::DOCUMENT_IDLE); + else { + error_message = "Invalid RunAtDeclaration " + value; + return false; + } + } else if (GetDeclarationValue(line, kUrlSourceDeclaration, &value) || + GetDeclarationValue(line, kUrlHomePageDeclaration, &value)) { + script->set_url_source(value); + } else if (GetDeclarationValue(line, kParserError, &value)) { + script->set_parser_error(value); + } else if (GetDeclarationValue(line, kForceDisabled, &value)) { + script->set_force_disabled(); + } + + // TODO(aa): Handle more types of metadata. + } + + line_start = line_end + 1; + } + + // If no patterns were specified, default to @include *. This is what + // Greasemonkey does. + if (script->globs().empty() && script->url_patterns().is_empty()) + script->add_glob("*"); + + return true; +} + +// static +bool LoadUserScriptFromFile( + const base::FilePath& user_script_path, const GURL& original_url, + std::unique_ptr& script, + bool* found_metadata, + std::u16string* error) { + + base::File infile; + if (user_script_path.IsContentUri()) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScriptLoader: path " << user_script_path << " is a content uri"; + + infile = OpenContentUriForRead(user_script_path); + } else { + infile = base::File(user_script_path, base::File::FLAG_OPEN | base::File::FLAG_READ); + } + + if (!infile.IsValid()) { + *error = u"Cannot open script source."; + return false; + } + + auto length = infile.GetLength(); + if (length<=0) { + *error = u"File is empty."; + return false; + } + + auto buffer = std::vector(length); + int bytes_read = infile.Read(0, buffer.data(), length); + if (bytes_read == -1) { + *error = u"Could not read source file."; + return false; + } + + std::string content(buffer.begin(), buffer.end()); + if (!base::IsStringUTF8(content)) { + *error = u"User script must be UTF8 encoded."; + return false; + } + + std::string detailed_error; + bool parseResult = UserScriptLoader::ParseMetadataHeader(content, script, + found_metadata, detailed_error); + if (parseResult == false || *found_metadata == false) { + std::u16string detailed_error16; + base::UTF8ToUTF16(detailed_error.c_str(), detailed_error.length(), &detailed_error16); + *error = base::StrCat({u"Invalid script header. ", detailed_error16}); + return false; + } + + script->set_match_origin_as_fallback(MatchOriginAsFallbackBehavior::kNever); + + // remove unicode chars and set content into File + stripUnicode(content); + std::unique_ptr file(new UserScript::File()); + file->set_content(content); + file->set_url(GURL(/*script_key*/ "script.js")); // name doesn't matter + + // create SHA256 of file + char raw[crypto::kSHA256Length] = {0}; + std::string key; + crypto::SHA256HashString(content, raw, crypto::kSHA256Length); + base::Base64Encode(base::StringPiece(raw, crypto::kSHA256Length), &key); + file->set_key(key); + + script->js_scripts().push_back(std::move(file)); + + // add into key the filename + // this value is used in ui to discriminate scripts + script->set_key(user_script_path.BaseName().value()); + return true; +} + +// static +bool GetOrCreatePath(base::FilePath& path) { + base::PathService::Get(base::DIR_ANDROID_APP_DATA, &path); + path = path.AppendASCII("userscripts"); + + // create snippets directory if not exists + if (!base::PathExists(path)) { + LOG(INFO) << "Path " << path << " doesn't exists. Creating"; + base::File::Error error = base::File::FILE_OK; + if (!base::CreateDirectoryAndGetError(path, &error)) { + LOG(ERROR) << + "UserScriptLoader: failed to create directory: " << path + << " with error code " << error; + return false; + } + } + return true; +} + +// static +void LoadUserScripts(UserScriptList* user_scripts_list) { + base::FilePath path; + if (GetOrCreatePath(path) == false) return; + + // enumerate all files from script path + // we accept all files, but we check if it's a real + // userscript + base::FileEnumerator dir_enum( + path, + /*recursive=*/false, base::FileEnumerator::FILES); + base::FilePath full_name; + while (full_name = dir_enum.Next(), !full_name.empty()) { + std::unique_ptr userscript(new UserScript()); + + std::u16string error; + bool found_metadata; + if (LoadUserScriptFromFile(full_name, GURL(), userscript, &found_metadata, &error)) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScriptLoader: Found user script " << userscript->name() << + "-" << userscript->version() << + "-" << userscript->description(); + + userscript->set_file_path(full_name.AsUTF8Unsafe()); + user_scripts_list->push_back(std::move(userscript)); + } else { + LOG(ERROR) << "UserScriptLoader: load error " << error; + } + } +} + +UserScriptLoader::UserScriptLoader(BrowserContext* browser_context, + UserScriptsPrefs* prefs) + : loaded_scripts_(new UserScriptList()), + ready_(false), + browser_context_(browser_context), + prefs_(prefs) {} + +UserScriptLoader::~UserScriptLoader() { + for (auto& observer : observers_) + observer.OnUserScriptLoaderDestroyed(this); +} + +void UserScriptLoader::OnRenderProcessHostCreated( + content::RenderProcessHost* process_host) { + if (initial_load_complete()) { + SendUpdate(process_host, shared_memory_); + } +} + +void UserScriptLoader::SetReady(bool ready) { + bool was_ready = ready_; + ready_ = ready; + if (ready_ && !was_ready) + AttemptLoad(); +} + +void UserScriptLoader::AttemptLoad() { + int tryOut = prefs_->GetCurrentStartupTryout(); + if (tryOut >= 3) { + LOG(INFO) << "UserScriptLoader: Possible crash detected. UserScript disabled"; + prefs_->SetEnabled(false); + } else { + prefs_->StartupTryout(tryOut+1); + StartLoad(); + } +} + +void UserScriptLoader::StartLoad() { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScriptLoader: StartLoad"; + + // Reload any loaded scripts, and clear out |loaded_scripts_| to indicate that + // the scripts aren't currently ready. + std::unique_ptr scripts_to_load = std::move(loaded_scripts_); + scripts_to_load->clear(); + + if (prefs_->IsEnabled()) { + LoadScripts(std::move(scripts_to_load), + base::BindOnce(&UserScriptLoader::OnScriptsLoaded, + weak_factory_.GetWeakPtr())); + } else { + OnScriptsLoaded(std::move(scripts_to_load)); + } +} + +// static +base::ReadOnlySharedMemoryRegion UserScriptLoader::Serialize( + const UserScriptList& scripts) { + base::Pickle pickle; + pickle.WriteUInt32(scripts.size()); + for (const std::unique_ptr& script : scripts) { + // TODO(aa): This can be replaced by sending content script metadata to + // renderers along with other extension data in ExtensionMsg_Loaded. + // See crbug.com/70516. + script->Pickle(&pickle); + // Write scripts as 'data' so that we can read it out in the slave without + // allocating a new string. + for (const std::unique_ptr& js_file : + script->js_scripts()) { + base::StringPiece contents = js_file->GetContent(); + pickle.WriteData(contents.data(), contents.length()); + } + for (const std::unique_ptr& css_file : + script->css_scripts()) { + base::StringPiece contents = css_file->GetContent(); + pickle.WriteData(contents.data(), contents.length()); + } + } + + // Create the shared memory object. + base::MappedReadOnlyRegion shared_memory = + base::ReadOnlySharedMemoryRegion::Create(pickle.size()); + if (!shared_memory.IsValid()) + return {}; + + // Copy the pickle to shared memory. + memcpy(shared_memory.mapping.memory(), pickle.data(), pickle.size()); + return std::move(shared_memory.region); +} + +void UserScriptLoader::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void UserScriptLoader::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +void UserScriptLoader::OnScriptsLoaded( + std::unique_ptr user_scripts) { + + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScriptLoader: OnScriptsLoaded"; + + // Check user preferences for loaded user scripts + prefs_->CompareWithPrefs(*user_scripts); + loaded_scripts_ = std::move(user_scripts); + + base::ReadOnlySharedMemoryRegion shared_memory; + shared_memory = + UserScriptLoader::Serialize(*loaded_scripts_); + + if (!shared_memory.IsValid()) { + // This can happen if we run out of file descriptors. In that case, we + // have a choice between silently omitting all user scripts for new tabs, + // by nulling out shared_memory_, or only silently omitting new ones by + // leaving the existing object in place. The second seems less bad, even + // though it removes the possibility that freeing the shared memory block + // would open up enough FDs for long enough for a retry to succeed. + + // Pretend the extension change didn't happen. + return; + } + + // We've got scripts ready to go. + shared_memory_ = std::move(shared_memory); + + for (content::RenderProcessHost::iterator i( + content::RenderProcessHost::AllHostsIterator()); + !i.IsAtEnd(); i.Advance()) { + SendUpdate(i.GetCurrentValue(), shared_memory_); + } + + // DCHECK(false); trying crash + prefs_->StartupTryout(0); + + for (auto& observer : observers_) + observer.OnScriptsLoaded(this, browser_context_); +} + +void UserScriptLoader::SendUpdate( + content::RenderProcessHost* process, + const base::ReadOnlySharedMemoryRegion& shared_memory) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScriptLoader: SendUpdate"; + + // If the process is being started asynchronously, early return. We'll end up + // calling InitUserScripts when it's created which will call this again. + base::ProcessHandle handle = process->GetProcess().Handle(); + if (!handle) + return; + + base::ReadOnlySharedMemoryRegion region_for_process = + shared_memory.Duplicate(); + if (!region_for_process.IsValid()) + return; + + process->Send(new ExtensionMsg_UpdateUserScripts( + std::move(region_for_process))); +} + +void LoadScriptsOnFileTaskRunner( + std::unique_ptr user_scripts, + UserScriptLoader::LoadScriptsCallback callback) { + DCHECK(GetUserScriptsFileTaskRunner()->RunsTasksInCurrentSequence()); + DCHECK(user_scripts.get()); + + // load user scripts from path + LoadUserScripts(user_scripts.get()); + + // Explicit priority to prevent unwanted task priority inheritance. + content::GetUIThreadTaskRunner({base::TaskPriority::USER_BLOCKING}) + ->PostTask(FROM_HERE, + base::BindOnce(std::move(callback), std::move(user_scripts))); +} + +void UserScriptLoader::LoadScripts( + std::unique_ptr user_scripts, + LoadScriptsCallback callback) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + GetUserScriptsFileTaskRunner()->PostTask( + FROM_HERE, + base::BindOnce(&LoadScriptsOnFileTaskRunner, std::move(user_scripts), + std::move(callback))); +} + +void RemoveScriptsOnFileTaskRunner( + const std::string& script_id, + UserScriptLoader::RemoveScriptCallback callback) { + DCHECK(GetUserScriptsFileTaskRunner()->RunsTasksInCurrentSequence()); + + base::FilePath path; + if (GetOrCreatePath(path)) { + base::FilePath file = path.Append(script_id); + if (base::DeleteFile(file) == false) { + LOG(ERROR) << + "ERROR: failed to delete file : " << path; + } + } + + content::GetUIThreadTaskRunner({base::TaskPriority::USER_BLOCKING}) + ->PostTask(FROM_HERE, + base::BindOnce(std::move(callback))); +} + +void UserScriptLoader::OnScriptRemoved() { + StartLoad(); +} + +void UserScriptLoader::RemoveScript(const std::string& script_id) { + if (!prefs_->IsEnabled()) return; + prefs_->RemoveScriptFromPrefs(script_id); + + GetUserScriptsFileTaskRunner()->PostTask( + FROM_HERE, + base::BindOnce(&RemoveScriptsOnFileTaskRunner, + std::move(script_id), + base::BindOnce(&UserScriptLoader::OnScriptRemoved, + weak_factory_.GetWeakPtr()))); +} + +void UserScriptLoader::SetScriptEnabled(const std::string& script_id, bool is_enabled) { + if (!prefs_->IsEnabled()) return; + prefs_->SetScriptEnabled(script_id, is_enabled); + StartLoad(); +} + +void UserScriptLoader::SelectAndAddScriptFromFile(ui::WindowAndroid* nativeWindow) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + if (!prefs_->IsEnabled()) return; + + dialog_ = ui::SelectFileDialog::Create( + this, std::make_unique(nullptr /*web_contents*/)); + + ui::SelectFileDialog::FileTypeInfo allowed_file_info; + allowed_file_info.extensions = {{FILE_PATH_LITERAL("js")}}; + allowed_file_info.allowed_paths = + ui::SelectFileDialog::FileTypeInfo::ANY_PATH; + base::FilePath suggested_name; + + std::vector types; + types.push_back(u"*/*"); /*= java SelectFileDialog.ALL_TYPES*/ + std::pair, bool> accept_types = std::make_pair( + types, false /*use_media_capture*/); + + dialog_->SelectFile( + ui::SelectFileDialog::SELECT_OPEN_FILE, + std::u16string() /* dialog title*/, suggested_name, &allowed_file_info, + 0 /* file type index */, std::string() /* default file extension */, + nativeWindow, + &accept_types /* params */); +} + + +void LoadScriptFromPathOnFileTaskRunner( + const base::FilePath& path, + const std::string& display_name, + UserScriptLoader::LoadSingleScriptCallback callback ) { + DCHECK(GetUserScriptsFileTaskRunner()->RunsTasksInCurrentSequence()); + + std::unique_ptr userscript(new UserScript()); + std::u16string error; + bool found_metadata = false; + bool loaded = LoadUserScriptFromFile(path, GURL(), userscript, &found_metadata, &error); + + bool result = loaded; + if (result || found_metadata) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScriptLoader: found " << userscript->name() << + "-" << userscript->version() << + "-" << userscript->description(); + base::FilePath destination; + if (GetOrCreatePath(destination) == false) { + error = u"Cannot create destination."; + } else { + // we need an unique file name + if (display_name.empty() == false) { + userscript->set_key(display_name); + } + + // filename is original filename or display_name + std::string file_name(userscript->key()); + base::i18n::ReplaceIllegalCharactersInPath(&file_name, '_'); + destination = destination.Append(file_name); + + if (destination.ReferencesParent()) { + error = u"Invalid file name."; + result = false; + } else if (base::PathExists(destination)) { + error = u"User script already loaded."; + result = false; + } else { + if (loaded) { + // if is a correct userscript, copy it + result = base::CopyFile(path, destination); + if (result == false) { + error = u"Copy error."; + } + } else { + // else, there is a parser error + // write minimal values and the error string, so UI can show it + std::string combined_string = base::StrCat({ + kUserScriptBegin, "\n", + kNamespaceDeclaration, " ", userscript->name_space(), "\n", + kNameDeclaration, " ", userscript->name(), "\n", + kVersionDeclaration, " ", userscript->version(), "\n", + kDescriptionDeclaration, " ", userscript->description(), "\n", + kUrlSourceDeclaration, " ", userscript->url_source(), "\n", + kParserError, " ", base::UTF16ToASCII(error), "\n", + kForceDisabled, " true\n", + kUserScriptEnd, "\n" + }); + + if (!base::WriteFile(destination, combined_string)) { + error = u"Cannot write."; + result = false; + } + } + } + } + } + + if (!error.empty()) { + LOG(ERROR) << "UserScriptLoader: load error " << error; + } + + // return to callback with eventually the error + const std::string string_error = base::UTF16ToASCII(error); + content::GetUIThreadTaskRunner({base::TaskPriority::USER_BLOCKING}) + ->PostTask(FROM_HERE, + base::BindOnce(std::move(callback), result, + std::move(string_error))); +} + +void UserScriptLoader::TryToInstall(const base::FilePath& script_path) { + if (!prefs_->IsEnabled()) return; + + std::u16string file_display_name; + base::MaybeGetFileDisplayName(script_path, &file_display_name); + + std::string display_name = script_path.BaseName().value(); + if (base::IsStringASCII(file_display_name)) + display_name = base::UTF16ToASCII(file_display_name); + + GetUserScriptsFileTaskRunner()->PostTask( + FROM_HERE, + base::BindOnce( + &LoadScriptFromPathOnFileTaskRunner, + script_path, display_name, + base::BindOnce( + &UserScriptLoader::LoadScriptFromPathOnFileTaskRunnerCallback, + weak_factory_.GetWeakPtr() + ) + )); +} + +void UserScriptLoader::FileSelected( + const base::FilePath& path, int index, void* params) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScriptLoader: FileSelected " << path; + + UserScriptLoader::TryToInstall(path); +} + +void UserScriptLoader::LoadScriptFromPathOnFileTaskRunnerCallback( + bool result, const std::string& error) { + for (auto& observer : observers_) + observer.OnUserScriptLoaded(this, result, error); + + StartLoad(); +} + +void UserScriptLoader::FileSelectionCanceled( + void* params) { +} + +} // namespace extensions diff --git a/components/user_scripts/browser/user_script_loader.h b/components/user_scripts/browser/user_script_loader.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/browser/user_script_loader.h @@ -0,0 +1,170 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#ifndef USERSCRIPTS_BROWSER_USER_SCRIPT_LOADER_H_ +#define USERSCRIPTS_BROWSER_USER_SCRIPT_LOADER_H_ + +#include +#include +#include + +#include "base/callback_forward.h" +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "base/memory/read_only_shared_memory_region.h" +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "content/public/browser/render_process_host_creation_observer.h" +#include "ui/shell_dialogs/select_file_dialog.h" +#include "content/browser/file_system_access/file_system_chooser.h" +#include "ui/android/window_android.h" + +#include "../common/host_id.h" +#include "../common/user_script.h" +#include "user_script_prefs.h" + +namespace base { +class ReadOnlySharedMemoryRegion; +} + +namespace content { +class BrowserContext; +class RenderProcessHost; +} + +namespace user_scripts { + +// Manages one "logical unit" of user scripts in shared memory by constructing a +// new shared memory region when the set of scripts changes. Also notifies +// renderers of new shared memory region when new renderers appear, or when +// script reloading completes. Script loading lives on the file thread. +class UserScriptLoader : public content::RenderProcessHostCreationObserver, + public ui::SelectFileDialog::Listener { + public: + using LoadScriptsCallback = + base::OnceCallback)>; + using LoadSingleScriptCallback = + base::OnceCallback; + + using RemoveScriptCallback = + base::OnceCallback; + class Observer { + public: + virtual void OnScriptsLoaded(UserScriptLoader* loader, + content::BrowserContext* browser_context) = 0; + virtual void OnUserScriptLoaderDestroyed(UserScriptLoader* loader) = 0; + virtual void OnUserScriptLoaded(UserScriptLoader* loader, + bool result, const std::string& error) = 0; + }; + + // Parses the includes out of |script| and returns them in |includes|. + static bool ParseMetadataHeader(const base::StringPiece& script_text, + std::unique_ptr& script, + bool *found_metadata, + std::string& error_message); + + UserScriptLoader(content::BrowserContext* browser_context, + UserScriptsPrefs* prefs); + //const HostID& host_id); + ~UserScriptLoader() override; + + // Initiates procedure to start loading scripts on the file thread. + void StartLoad(); + + // Returns true if we have any scripts ready. + bool initial_load_complete() const { return shared_memory_.IsValid(); } + + // Pickle user scripts and return pointer to the shared memory. + static base::ReadOnlySharedMemoryRegion Serialize( + const user_scripts::UserScriptList& scripts); + + // Adds or removes observers. + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Sets the flag if the initial set of hosts has finished loading; if it's + // set to be true, calls AttempLoad() to bootstrap. + void SetReady(bool ready); + + void RemoveScript(const std::string& script_id); + void SetScriptEnabled(const std::string& script_id, bool is_enabled); + + void SelectAndAddScriptFromFile(ui::WindowAndroid* wa); + void TryToInstall(const base::FilePath& script_path); + + void LoadScripts(std::unique_ptr user_scripts, + LoadScriptsCallback callback); + + protected: + content::BrowserContext* browser_context() const { return browser_context_; } + + UserScriptsPrefs* prefs() const { return prefs_; } + + private: + void OnRenderProcessHostCreated( + content::RenderProcessHost* process_host) override; + + // Attempts to initiate a load. + void AttemptLoad(); + + // Called once we have finished loading the scripts on the file thread. + void OnScriptsLoaded(std::unique_ptr user_scripts); + + // Sends the renderer process a new set of user scripts. If + // |changed_hosts| is not empty, this signals that only the scripts from + // those hosts should be updated. Otherwise, all hosts will be + // updated. + void SendUpdate(content::RenderProcessHost* process, + const base::ReadOnlySharedMemoryRegion& shared_memory); + + // Contains the scripts that were found the last time scripts were updated. + base::ReadOnlySharedMemoryRegion shared_memory_; + + // List of scripts that are currently loaded. This is null when a load is in + // progress. + std::unique_ptr loaded_scripts_; + + // If the initial set of hosts has finished loading. + bool ready_; + + // The browser_context for which the scripts managed here are installed. + content::BrowserContext* browser_context_; + + // Manage load and store from preferences + UserScriptsPrefs* prefs_; + + // The associated observers. + base::ObserverList::Unchecked observers_; + + void OnScriptRemoved(); + + // Manage file dialog requests + scoped_refptr dialog_; + void FileSelected(const base::FilePath& path, + int index, void* params) override; + void FileSelectionCanceled(void* params) override; + void LoadScriptFromPathOnFileTaskRunnerCallback( + bool result, const std::string& error ); + + base::WeakPtrFactory weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(UserScriptLoader); +}; + +} // namespace extensions + +#endif // USERSCRIPTS_BROWSER_USER_SCRIPT_LOADER_H_ diff --git a/components/user_scripts/browser/user_script_pref_info.cc b/components/user_scripts/browser/user_script_pref_info.cc new file mode 100644 --- /dev/null +++ b/components/user_scripts/browser/user_script_pref_info.cc @@ -0,0 +1,34 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#include "user_script_pref_info.h" + +namespace user_scripts { + +UserScriptsListPrefs::ScriptInfo::ScriptInfo(const std::string& name, + const std::string& description, + const base::Time& install_time, + bool enabled) + : install_time(install_time), + enabled(enabled), + name_(name), + description_(description) {} + +UserScriptsListPrefs::ScriptInfo::ScriptInfo(const ScriptInfo& other) = default; +UserScriptsListPrefs::ScriptInfo::~ScriptInfo() = default; + +} \ No newline at end of file diff --git a/components/user_scripts/browser/user_script_pref_info.h b/components/user_scripts/browser/user_script_pref_info.h new file mode 100644 --- /dev/null +++ b/components/user_scripts/browser/user_script_pref_info.h @@ -0,0 +1,72 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#ifndef USERSCRIPTS_BROWSER_USERSCRIPT_PREF_INFO_H_ +#define USERSCRIPTS_BROWSER_USERSCRIPT_PREF_INFO_H_ + +#include "base/values.h" +#include "base/time/time.h" +#include "components/keyed_service/core/keyed_service.h" + +namespace user_scripts { + +class UserScriptsListPrefs : public KeyedService { + public: + struct ScriptInfo { + ScriptInfo(const std::string& name, + const std::string& description, + const base::Time& install_time, + bool enabled); + ScriptInfo(const ScriptInfo& other); + ~ScriptInfo(); + + 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; } + + const std::string& version() const { return version_; } + void set_version(const std::string& version) { version_ = version; } + + const std::string& file_path() const { return file_path_; } + void set_file_path(const std::string& file_path) { file_path_ = file_path; } + + const std::string& url_source() const { return url_source_; } + void set_url_source(const std::string& url_source) { url_source_ = url_source; } + + const std::string& parser_error() const { return parser_error_; } + void set_parser_error(const std::string& parser_error) { parser_error_ = parser_error; } + + base::Time install_time; + bool enabled; + + bool force_disabled; + + private: + std::string name_; + std::string description_; + std::string version_; + std::string file_path_; + std::string url_source_; + std::string parser_error_; + }; +}; + +} + +#endif // USERSCRIPTS_BROWSER_USERSCRIPT_PREF_INFO_H_ \ No newline at end of file diff --git a/components/user_scripts/browser/user_script_prefs.cc b/components/user_scripts/browser/user_script_prefs.cc new file mode 100644 --- /dev/null +++ b/components/user_scripts/browser/user_script_prefs.cc @@ -0,0 +1,276 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#include + +#include "base/values.h" +#include "base/strings/string_number_conversions.h" +#include "base/json/json_writer.h" + +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_manager.h" + +#include "components/prefs/pref_service.h" +#include "components/prefs/scoped_user_pref_update.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "user_script_prefs.h" +#include "user_script_pref_info.h" +#include "../common/user_script.h" +#include "../common/user_scripts_features.h" + +namespace user_scripts { + +namespace prefs { + const char kUserScriptsEnabled[] = "userscripts.enabled"; +} + +namespace { + +const char kUserScriptsStartup[] = "userscripts.startup"; + +const char kUserScriptsList[] = "userscripts.scripts"; +const char kScriptIsEnabled[] = "enabled"; +const char kScriptName[] = "name"; +const char kScriptDescription[] = "description"; +const char kScriptVersion[] = "version"; +const char kScriptInstallTime[] = "install_time"; +const char kScriptFilePath[] = "file_path"; +const char kScriptUrlSource[] = "url_source"; +const char kScriptParserError[] = "parser_error"; +const char kScriptForceDisabled[] = "force_disabled"; + +class PrefUpdate : public DictionaryPrefUpdate { + public: + PrefUpdate(PrefService* service, + const std::string& id, + const std::string& path) + : DictionaryPrefUpdate(service, path), id_(id) {} + + ~PrefUpdate() override = default; + + base::DictionaryValue* Get() override { + base::DictionaryValue* dict = DictionaryPrefUpdate::Get(); + base::Value* dict_item = + dict->FindKeyOfType(id_, base::Value::Type::DICTIONARY); + if (!dict_item) + dict_item = dict->SetKey(id_, base::Value(base::Value::Type::DICTIONARY)); + return static_cast(dict_item); + } + + private: + const std::string id_; + + DISALLOW_COPY_AND_ASSIGN(PrefUpdate); +}; + +bool GetInt64FromPref(const base::DictionaryValue* dict, + const std::string& key, + int64_t* value) { + DCHECK(dict); + const std::string* value_str = dict->FindStringKey(key); + if (!value_str) { + VLOG(2) << "Can't find key in local pref dictionary. Invalid key: " << key + << "."; + return false; + } + + if (!base::StringToInt64(*value_str, value)) { + VLOG(2) << "Can't change string to int64_t. Invalid string value: " + << *value_str << "."; + return false; + } + + return true; +} + +} + +UserScriptsPrefs::UserScriptsPrefs( + PrefService* prefs) + : prefs_(prefs) { +} + +// static +void UserScriptsPrefs::RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterBooleanPref(prefs::kUserScriptsEnabled, false); + registry->RegisterIntegerPref(kUserScriptsStartup, 0); + registry->RegisterDictionaryPref(kUserScriptsList); +} + +bool UserScriptsPrefs::IsEnabled() { + return prefs_->GetBoolean(prefs::kUserScriptsEnabled); +} + +void UserScriptsPrefs::SetEnabled(bool enabled) { + prefs_->SetBoolean(prefs::kUserScriptsEnabled, enabled); + prefs_->CommitPendingWrite(); +} + +void UserScriptsPrefs::StartupTryout(int number) { + prefs_->SetInteger(kUserScriptsStartup, number); + prefs_->CommitPendingWrite(); +} + +int UserScriptsPrefs::GetCurrentStartupTryout() { + return prefs_->GetInteger(kUserScriptsStartup); +} + +void UserScriptsPrefs::CompareWithPrefs(UserScriptList& user_scripts) { + if (IsEnabled() == false) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScriptsPrefs: disabled by user"; + + user_scripts.clear(); + return; + } + + std::vector all_scripts; + + auto it = user_scripts.begin(); + while (it != user_scripts.end()) + { + std::string key = it->get()->key(); + all_scripts.push_back(key); + + std::unique_ptr scriptInfo = + UserScriptsPrefs::CreateScriptInfoFromPrefs(key); + + // add or update prefs + scriptInfo->set_name(it->get()->name()); + scriptInfo->set_description(it->get()->description()); + scriptInfo->set_version(it->get()->version()); + scriptInfo->set_file_path(it->get()->file_path()); + scriptInfo->set_url_source(it->get()->url_source()); + scriptInfo->set_parser_error(it->get()->parser_error()); + scriptInfo->force_disabled = (it->get()->force_disabled()); + + PrefUpdate update(prefs_, key, kUserScriptsList); + base::DictionaryValue* script_dict = update.Get(); + + script_dict->SetString(kScriptName, scriptInfo->name()); + script_dict->SetString(kScriptDescription, scriptInfo->description()); + script_dict->SetBoolean(kScriptIsEnabled, scriptInfo->enabled); + script_dict->SetString(kScriptVersion, scriptInfo->version()); + script_dict->SetString(kScriptFilePath, scriptInfo->file_path()); + script_dict->SetString(kScriptUrlSource, scriptInfo->url_source()); + script_dict->SetString(kScriptParserError, scriptInfo->parser_error()); + script_dict->SetBoolean(kScriptForceDisabled, scriptInfo->force_disabled); + + std::string install_time_str = + base::NumberToString(scriptInfo->install_time.ToInternalValue()); + script_dict->SetString(kScriptInstallTime, install_time_str); + + if (!scriptInfo->enabled) { + it = user_scripts.erase(it); + } else { + ++it; + } + } + + // remove script from prefs if no more present + std::vector all_scripts_to_remove; + const base::DictionaryValue* dict = + prefs_->GetDictionary(kUserScriptsList); + for (base::DictionaryValue::Iterator script_it(*dict); !script_it.IsAtEnd(); + script_it.Advance()) { + const std::string& key = script_it.key(); + + if (std::find(all_scripts.begin(), all_scripts.end(), key) == all_scripts.end()) { + all_scripts_to_remove.push_back(key); + } + } + + DictionaryPrefUpdate update(prefs_, kUserScriptsList); + base::DictionaryValue* const update_dict = update.Get(); + for (auto key : all_scripts_to_remove) { + update_dict->RemoveKey(key); + } + + return; +} + +std::string UserScriptsPrefs::GetScriptsInfo() { + std::string json_string; + + const base::DictionaryValue* dict = + prefs_->GetDictionary(kUserScriptsList); + + if (dict) { + base::JSONWriter::WriteWithOptions( + *dict, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json_string); + base::TrimWhitespaceASCII(json_string, base::TRIM_ALL, &json_string); + } + + return json_string; +} + +std::unique_ptr UserScriptsPrefs::CreateScriptInfoFromPrefs( + const std::string& script_id) const { + + auto scriptInfo = std::make_unique( + script_id, "", base::Time::Now(), false); + + const base::DictionaryValue* scripts = + prefs_->GetDictionary(kUserScriptsList); + if (!scripts) + return scriptInfo; + + const base::DictionaryValue* script = static_cast( + scripts->FindDictKey(script_id)); + if (!script) + return scriptInfo; + + const std::string* name = script->FindStringKey(kScriptName); + const std::string* description = script->FindStringKey(kScriptDescription); + const std::string* version = script->FindStringKey(kScriptVersion); + const std::string* file_path = script->FindStringKey(kScriptFilePath); + const std::string* url_source = script->FindStringKey(kScriptUrlSource); + const std::string* parser_error = script->FindStringKey(kScriptParserError); + + scriptInfo->set_name( name ? *name : "no name" ); + scriptInfo->set_description( description ? *description : "no description" ); + scriptInfo->set_version( version ? *version : "no version" ); + scriptInfo->enabled = script->FindBoolKey(kScriptIsEnabled).value_or(false); + scriptInfo->set_file_path( file_path ? *file_path : "no file path" ); + scriptInfo->set_url_source( url_source ? *url_source : "" ); + scriptInfo->set_parser_error( parser_error ? *parser_error : "" ); + scriptInfo->force_disabled = script->FindBoolKey(kScriptForceDisabled).value_or(false); + + int64_t time_interval = 0; + if (GetInt64FromPref(script, kScriptInstallTime, &time_interval)) { + scriptInfo->install_time = base::Time::FromInternalValue(time_interval); + } + + return scriptInfo; +} + +void UserScriptsPrefs::RemoveScriptFromPrefs(const std::string& script_id) { + DictionaryPrefUpdate update(prefs_, kUserScriptsList); + base::DictionaryValue* const update_dict = update.Get(); + update_dict->RemoveKey(script_id); +} + +void UserScriptsPrefs::SetScriptEnabled(const std::string& script_id, bool is_enabled) { + PrefUpdate update(prefs_, script_id, kUserScriptsList); + base::DictionaryValue* script_dict = update.Get(); + if (script_dict->FindBoolKey(kScriptForceDisabled).value_or(false)) + is_enabled = true; + script_dict->SetBoolean(kScriptIsEnabled, is_enabled); +} + +} diff --git a/components/user_scripts/browser/user_script_prefs.h b/components/user_scripts/browser/user_script_prefs.h new file mode 100644 --- /dev/null +++ b/components/user_scripts/browser/user_script_prefs.h @@ -0,0 +1,62 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#ifndef USERSCRIPTS_BROWSER_USERSCRIPT_PREFS_H_ +#define USERSCRIPTS_BROWSER_USERSCRIPT_PREFS_H_ + +#include "content/public/browser/browser_context.h" +#include "components/prefs/pref_service.h" +#include "components/pref_registry/pref_registry_syncable.h" + +#include "user_script_pref_info.h" +#include "../common/user_script.h" + +namespace user_scripts { + +namespace prefs { + extern const char kUserScriptsEnabled[]; +} + +class UserScriptsPrefs { + public: + UserScriptsPrefs( + PrefService* prefs); + + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + bool IsEnabled(); + void SetEnabled(bool enabled); + + void StartupTryout(int number); + int GetCurrentStartupTryout(); + + void CompareWithPrefs(UserScriptList& user_scripts); + + std::string GetScriptsInfo(); + void RemoveScriptFromPrefs(const std::string& script_id); + void SetScriptEnabled(const std::string& script_id, bool is_enabled); + + std::unique_ptr CreateScriptInfoFromPrefs( + const std::string& script_id) const; + + private: + PrefService* prefs_; +}; + +} + +#endif // USERSCRIPTS_BROWSER_USERSCRIPT_PREFS_H_ \ No newline at end of file diff --git a/components/user_scripts/browser/userscripts_browser_client.cc b/components/user_scripts/browser/userscripts_browser_client.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/browser/userscripts_browser_client.cc @@ -0,0 +1,78 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#include "userscripts_browser_client.h" + +#include "base/logging.h" + +#include "chrome/browser/browser_process.h" + +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_manager.h" + +#include "../common/user_scripts_features.h" +#include "user_script_loader.h" +#include "file_task_runner.h" +#include "user_script_prefs.h" + +namespace user_scripts { + +namespace { + +// remember: was ExtensionsBrowserClient +UserScriptsBrowserClient* g_userscripts_browser_client = NULL; + +} // namespace + +UserScriptsBrowserClient::UserScriptsBrowserClient() {} + +UserScriptsBrowserClient::~UserScriptsBrowserClient() = default; + +// static +UserScriptsBrowserClient* UserScriptsBrowserClient::GetInstance() { + // only for browser process + if (!g_browser_process) + return NULL; + + // singleton + if (g_userscripts_browser_client) + return g_userscripts_browser_client; + + // make file task runner + GetUserScriptsFileTaskRunner().get(); + + // new instance singleton + g_userscripts_browser_client = new UserScriptsBrowserClient(); + + return g_userscripts_browser_client; +} + +void UserScriptsBrowserClient::SetProfile(content::BrowserContext* context) { + browser_context_ = context; + + prefs_ = + std::make_unique( + static_cast(context)->GetPrefs()); + + userscript_loader_ = + std::make_unique(browser_context_, prefs_.get()); + if (prefs_->IsEnabled()) { + userscript_loader_->SetReady(true); + } +} + +} // namespace user_scripts diff --git a/components/user_scripts/browser/userscripts_browser_client.h b/components/user_scripts/browser/userscripts_browser_client.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/browser/userscripts_browser_client.h @@ -0,0 +1,62 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#ifndef USERSCRIPTS_BROWSER_USERSCRIPTS_BROWSER_CLIENT_H_ +#define USERSCRIPTS_BROWSER_USERSCRIPTS_BROWSER_CLIENT_H_ + +#include +#include +#include + +#include "content/public/browser/browser_context.h" + +#include "../common/user_script.h" +#include "user_script_loader.h" +#include "user_script_prefs.h" + +namespace user_scripts { + +class UserScriptsBrowserClient { + public: + UserScriptsBrowserClient(); + UserScriptsBrowserClient(const UserScriptsBrowserClient&) = delete; + UserScriptsBrowserClient& operator=(const UserScriptsBrowserClient&) = delete; + virtual ~UserScriptsBrowserClient(); + + // Returns the single instance of |this|. + static UserScriptsBrowserClient* GetInstance(); + + void SetProfile(content::BrowserContext* context); + + user_scripts::UserScriptsPrefs* GetPrefs() { + return prefs_.get(); + } + + user_scripts::UserScriptLoader* GetLoader() { + return userscript_loader_.get(); + } + + private: + std::unique_ptr scripts_; + content::BrowserContext* browser_context_; + std::unique_ptr prefs_; + std::unique_ptr userscript_loader_; +}; + +} // namespace extensions + +#endif // USERSCRIPTS_BROWSER_USERSCRIPTS_BROWSER_CLIENT_H_ diff --git a/components/user_scripts/common/BUILD.gn b/components/user_scripts/common/BUILD.gn new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/BUILD.gn @@ -0,0 +1,49 @@ +# Copyright 2014 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. + +import("//build/config/features.gni") +import("//mojo/public/tools/bindings/mojom.gni") + +static_library("common") { + sources = [ + "user_scripts_features.cc", + "user_scripts_features.h", + "constants.h", + "host_id.cc", + "host_id.h", + "script_constants.h", + "url_pattern_set.cc", + "url_pattern_set.h", + "url_pattern.cc", + "url_pattern.h", + "user_script.cc", + "user_script.h", + "view_type.cc", + "view_type.h", + "extension_messages.cc", + "extension_messages.h", + "extension_message_generator.cc", + "extension_message_generator.h", + ] + + configs += [ + "//build/config:precompiled_headers", + "//build/config/compiler:wexit_time_destructors", + ] + + public_deps = [ + "//components/services/app_service/public/cpp:app_file_handling", + "//content/public/common", + "//ipc", + "//skia", + ] + + deps = [ + "//base", + "//components/url_formatter", + "//components/url_matcher", + "//components/version_info", + "//crypto", + ] +} diff --git a/components/user_scripts/common/constants.h b/components/user_scripts/common/constants.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/constants.h @@ -0,0 +1,21 @@ +// 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. + +#ifndef USERSCRIPTS_COMMON_CONSTANTS_H_ +#define USERSCRIPTS_COMMON_CONSTANTS_H_ + +#include "base/files/file_path.h" +#include "base/strings/string_piece_forward.h" +#include "components/services/app_service/public/mojom/types.mojom.h" +#include "components/version_info/channel.h" +#include "ui/base/layout.h" + +namespace user_scripts { + +// The origin of injected CSS. +enum CSSOrigin { /*CSS_ORIGIN_AUTHOR,*/ CSS_ORIGIN_USER }; + +} // namespace user_scripts + +#endif // USERSCRIPTS_COMMON_CONSTANTS_H_ diff --git a/components/user_scripts/common/error_utils.cc b/components/user_scripts/common/error_utils.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/error_utils.cc @@ -0,0 +1,54 @@ +// Copyright (c) 2011 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 "error_utils.h" + +#include + +#include "base/check_op.h" +#include "base/strings/string_tokenizer.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" + +namespace user_scripts { + +namespace { + +std::string FormatErrorMessageInternal( + base::StringPiece format, + std::initializer_list args) { + std::string format_str = format.as_string(); + base::StringTokenizer tokenizer(format_str, "*"); + tokenizer.set_options(base::StringTokenizer::RETURN_DELIMS); + + std::vector result_pieces; + auto* args_it = args.begin(); + while (tokenizer.GetNext()) { + if (!tokenizer.token_is_delim()) { + result_pieces.push_back(tokenizer.token_piece()); + continue; + } + + CHECK_NE(args_it, args.end()) + << "More placeholders (*) than substitutions."; + + // Substitute the argument. + result_pieces.push_back(*args_it); + args_it++; + } + + // Not all substitutions were consumed. + CHECK_EQ(args_it, args.end()) << "Fewer placeholders (*) than substitutions."; + + return base::JoinString(result_pieces, "" /* separator */); +} + +} // namespace + +std::string ErrorUtils::FormatErrorMessage(base::StringPiece format, + base::StringPiece s1) { + return FormatErrorMessageInternal(format, {s1}); +} + +} // namespace user_scripts diff --git a/components/user_scripts/common/error_utils.h b/components/user_scripts/common/error_utils.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/error_utils.h @@ -0,0 +1,24 @@ +// Copyright (c) 2011 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 USERSCRIPTS_COMMON_ERROR_UTILS_H_ +#define USERSCRIPTS_COMMON_ERROR_UTILS_H_ + +#include + +#include "base/strings/string_piece.h" + +namespace user_scripts { + +class ErrorUtils { + public: + // Creates an error messages from a pattern. + static std::string FormatErrorMessage(base::StringPiece format, + base::StringPiece s1); + +}; + +} // namespace extensions + +#endif // USERSCRIPTS_COMMON_ERROR_UTILS_H_ diff --git a/components/user_scripts/common/extension_message_generator.cc b/components/user_scripts/common/extension_message_generator.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/extension_message_generator.cc @@ -0,0 +1,29 @@ +// Copyright 2014 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. + +// Get basic type definitions. +#define IPC_MESSAGE_IMPL +#include "components/user_scripts/common/extension_message_generator.h" + +// Generate constructors. +#include "ipc/struct_constructor_macros.h" +#include "components/user_scripts/common/extension_message_generator.h" + +// Generate param traits write methods. +#include "ipc/param_traits_write_macros.h" +namespace IPC { +#include "components/user_scripts/common/extension_message_generator.h" +} // namespace IPC + +// Generate param traits read methods. +#include "ipc/param_traits_read_macros.h" +namespace IPC { +#include "components/user_scripts/common/extension_message_generator.h" +} // namespace IPC + +// Generate param traits log methods. +#include "ipc/param_traits_log_macros.h" +namespace IPC { +#include "components/user_scripts/common/extension_message_generator.h" +} // namespace IPC diff --git a/components/user_scripts/common/extension_message_generator.h b/components/user_scripts/common/extension_message_generator.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/extension_message_generator.h @@ -0,0 +1,11 @@ +// Copyright 2014 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. + +// Multiply-included file, hence no include guard. + +#undef USERSCRIPTS_COMMON_EXTENSION_MESSAGES_H_ +#include "extension_messages.h" +#ifndef USERSCRIPTS_COMMON_EXTENSION_MESSAGES_H_ +#error "Failed to include header extension_messages.h" +#endif diff --git a/components/user_scripts/common/extension_messages.cc b/components/user_scripts/common/extension_messages.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/extension_messages.cc @@ -0,0 +1,40 @@ +// Copyright 2014 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 "extension_messages.h" + +#include + +#include +#include + +#include "content/public/common/common_param_traits.h" + +namespace IPC { + +void ParamTraits::Write(base::Pickle* m, const param_type& p) { + WriteParam(m, p.type()); + WriteParam(m, p.id()); +} + +bool ParamTraits::Read(const base::Pickle* m, + base::PickleIterator* iter, + param_type* r) { + HostID::HostType type; + std::string id; + if (!ReadParam(m, iter, &type)) + return false; + if (!ReadParam(m, iter, &id)) + return false; + *r = HostID(type, id); + return true; +} + +void ParamTraits::Log( + const param_type& p, std::string* l) { + LogParam(p.type(), l); + LogParam(p.id(), l); +} + +} // namespace IPC diff --git a/components/user_scripts/common/extension_messages.h b/components/user_scripts/common/extension_messages.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/extension_messages.h @@ -0,0 +1,71 @@ +// Copyright 2014 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. + +// IPC messages for extensions. + +#ifndef USERSCRIPTS_COMMON_EXTENSION_MESSAGES_H_ +#define USERSCRIPTS_COMMON_EXTENSION_MESSAGES_H_ + +#include + +#include +#include +#include +#include +#include + +#include "base/macros.h" +#include "base/memory/read_only_shared_memory_region.h" +#include "base/values.h" +#include "content/public/common/common_param_traits.h" +#include "constants.h" +#include "host_id.h" +#include "ipc/ipc_message_start.h" +#include "ipc/ipc_message_macros.h" +#include "url/gurl.h" +#include "url/origin.h" + +#define IPC_MESSAGE_START ExtensionMsgStart + +IPC_ENUM_TRAITS_MAX_VALUE(HostID::HostType, HostID::HOST_TYPE_LAST) + +// Singly-included section for custom IPC traits. +#ifndef INTERNAL_USERSCRIPTS_COMMON_EXTENSION_MESSAGES_H_ +#define INTERNAL_USERSCRIPTS_COMMON_EXTENSION_MESSAGES_H_ + +namespace IPC { + +template <> +struct ParamTraits { + typedef HostID param_type; + static void Write(base::Pickle* m, const param_type& p); + static bool Read(const base::Pickle* m, + base::PickleIterator* iter, + param_type* r); + static void Log(const param_type& p, std::string* l); +}; + + +} // namespace IPC + +#endif // INTERNAL_USERSCRIPTS_COMMON_EXTENSION_MESSAGES_H_ + +// Notification that the user scripts have been updated. It has one +// ReadOnlySharedMemoryRegion argument consisting of the pickled script data. +// This memory region is valid in the context of the renderer. +// If |owner| is not empty, then the shared memory handle refers to |owner|'s +// programmatically-defined scripts. Otherwise, the handle refers to all +// hosts' statically defined scripts. So far, only extension-hosts support +// statically defined scripts; WebUI-hosts don't. +// If |changed_hosts| is not empty, only the host in that set will +// be updated. Otherwise, all hosts that have scripts in the shared memory +// region will be updated. Note that the empty set => all hosts case is not +// supported for per-extension programmatically-defined script regions; in such +// regions, the owner is expected to list itself as the only changed host. +// If |whitelisted_only| is true, this process should only run whitelisted +// scripts and not all user scripts. +IPC_MESSAGE_CONTROL1(ExtensionMsg_UpdateUserScripts, + base::ReadOnlySharedMemoryRegion) + +#endif // USERSCRIPTS_COMMON_EXTENSION_MESSAGES_H_ diff --git a/components/user_scripts/common/host_id.cc b/components/user_scripts/common/host_id.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/host_id.cc @@ -0,0 +1,31 @@ +// Copyright 2015 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 "host_id.h" + +#include + +HostID::HostID() + : type_(HostType::EXTENSIONS) { +} + +HostID::HostID(HostType type, const std::string& id) + : type_(type), id_(id) { +} + +HostID::HostID(const HostID& host_id) + : type_(host_id.type()), + id_(host_id.id()) { +} + +HostID::~HostID() { +} + +bool HostID::operator<(const HostID& host_id) const { + return std::tie(type_, id_) < std::tie(host_id.type_, host_id.id_); +} + +bool HostID::operator==(const HostID& host_id) const { + return type_ == host_id.type_ && id_ == host_id.id_; +} diff --git a/components/user_scripts/common/host_id.h b/components/user_scripts/common/host_id.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/host_id.h @@ -0,0 +1,35 @@ +// Copyright 2015 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 USERSCRIPTS_COMMON_HOST_ID_H_ +#define USERSCRIPTS_COMMON_HOST_ID_H_ + +#include + +// IDs of hosts who own user scripts. +// A HostID is immutable after creation. +struct HostID { + enum HostType { EXTENSIONS, WEBUI, HOST_TYPE_LAST = WEBUI }; + + HostID(); + HostID(HostType type, const std::string& id); + HostID(const HostID& host_id); + ~HostID(); + + bool operator<(const HostID& host_id) const; + bool operator==(const HostID& host_id) const; + + HostType type() const { return type_; } + const std::string& id() const { return id_; } + + private: + // The type of the host. + HostType type_; + + // Similar to extension_id, host_id is a unique indentifier for a host, + // e.g., an Extension or WebUI. + std::string id_; +}; + +#endif // USERSCRIPTS_COMMON_HOST_ID_H_ diff --git a/components/user_scripts/common/script_constants.h b/components/user_scripts/common/script_constants.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/script_constants.h @@ -0,0 +1,33 @@ +// Copyright 2020 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 USERSCRIPTS_COMMON_SCRIPT_CONSTANTS_H_ +#define USERSCRIPTS_COMMON_SCRIPT_CONSTANTS_H_ + +namespace user_scripts { + +// Whether to fall back to matching the origin for frames where the URL +// cannot be matched directly, such as those with about: or data: schemes. +enum class MatchOriginAsFallbackBehavior { + // Never fall back on the origin; this means scripts will never match on + // these frames. + kNever, + // Match the origin only for about:-scheme frames, and then climb the frame + // tree to find an appropriate ancestor to get a full URL (including path). + // This is for supporting the "match_about_blank" key. + // TODO(devlin): I wonder if we could simplify this to be "MatchForAbout", + // and not worry about climbing the frame tree. It would be a behavior + // change, but I wonder how many extensions it would impact in practice. + kMatchForAboutSchemeAndClimbTree, + // Match the origin as a fallback whenever applicable. This won't have a + // corresponding path. + kAlways, +}; + +// TODO(devlin): Move the other non-UserScript-specific constants like +// RunLocation and InjectionType from UserScript into here. + +} + +#endif // USERSCRIPTS_COMMON_SCRIPT_CONSTANTS_H_ diff --git a/components/user_scripts/common/url_pattern.cc b/components/user_scripts/common/url_pattern.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/url_pattern.cc @@ -0,0 +1,803 @@ +// 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. + +#include "url_pattern.h" + +#include + +#include + +#include "base/logging.h" +#include "base/strings/pattern.h" +#include "base/strings/strcat.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "content/public/common/url_constants.h" +#include "constants.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include "net/base/url_util.h" +#include "url/gurl.h" +#include "url/url_util.h" + +const char URLPattern::kAllUrlsPattern[] = ""; + +namespace { + +// TODO(aa): What about more obscure schemes like javascript: ? +// Note: keep this array in sync with kValidSchemeMasks. +const char* const kValidSchemes[] = { + url::kHttpScheme, url::kHttpsScheme, + url::kFileScheme, url::kFtpScheme, + /*content::kChromeUIScheme,*/ /*extensions::kExtensionScheme,*/ + url::kFileSystemScheme, url::kWsScheme, + url::kWssScheme, url::kDataScheme, + url::kUrnScheme, +}; + +const int kValidSchemeMasks[] = { + URLPattern::SCHEME_HTTP, URLPattern::SCHEME_HTTPS, + URLPattern::SCHEME_FILE, URLPattern::SCHEME_FTP, + /*URLPattern::SCHEME_CHROMEUI,*/ /*URLPattern::SCHEME_EXTENSION,*/ + URLPattern::SCHEME_FILESYSTEM, URLPattern::SCHEME_WS, + URLPattern::SCHEME_WSS, URLPattern::SCHEME_DATA, + URLPattern::SCHEME_URN, +}; + +static_assert(base::size(kValidSchemes) == base::size(kValidSchemeMasks), + "must keep these arrays in sync"); + +const char kParseSuccess[] = "Success."; +const char kParseErrorMissingSchemeSeparator[] = "Missing scheme separator."; +const char kParseErrorInvalidScheme[] = "Invalid scheme."; +const char kParseErrorWrongSchemeType[] = "Wrong scheme type."; +const char kParseErrorEmptyHost[] = "Host can not be empty."; +const char kParseErrorInvalidHostWildcard[] = "Invalid host wildcard."; +const char kParseErrorEmptyPath[] = "Empty path."; +const char kParseErrorInvalidPort[] = "Invalid port."; +const char kParseErrorInvalidHost[] = "Invalid host."; + +// Message explaining each URLPattern::ParseResult. +const char* const kParseResultMessages[] = { + kParseSuccess, + kParseErrorMissingSchemeSeparator, + kParseErrorInvalidScheme, + kParseErrorWrongSchemeType, + kParseErrorEmptyHost, + kParseErrorInvalidHostWildcard, + kParseErrorEmptyPath, + kParseErrorInvalidPort, + kParseErrorInvalidHost, +}; + +static_assert(static_cast(URLPattern::ParseResult::kNumParseResults) == + base::size(kParseResultMessages), + "must add message for each parse result"); + +const char kPathSeparator[] = "/"; + +bool IsStandardScheme(base::StringPiece scheme) { + // "*" gets the same treatment as a standard scheme. + if (scheme == "*") + return true; + + return url::IsStandard(scheme.data(), + url::Component(0, static_cast(scheme.length()))); +} + +bool IsValidPortForScheme(base::StringPiece scheme, base::StringPiece port) { + if (port == "*") + return true; + + // Only accept non-wildcard ports if the scheme uses ports. + if (url::DefaultPortForScheme(scheme.data(), scheme.length()) == + url::PORT_UNSPECIFIED) { + return false; + } + + int parsed_port = url::PORT_UNSPECIFIED; + if (!base::StringToInt(port, &parsed_port)) + return false; + return (parsed_port >= 0) && (parsed_port < 65536); +} + +// Returns |path| with the trailing wildcard stripped if one existed. +// +// The functions that rely on this (OverlapsWith and Contains) are only +// called for the patterns inside URLPatternSet. In those cases, we know that +// the path will have only a single wildcard at the end. This makes figuring +// out overlap much easier. It seems like there is probably a computer-sciency +// way to solve the general case, but we don't need that yet. +base::StringPiece StripTrailingWildcard(base::StringPiece path) { + if (base::EndsWith(path, "*")) + path.remove_suffix(1); + return path; +} + +// Removes trailing dot from |host_piece| if any. +base::StringPiece CanonicalizeHostForMatching(base::StringPiece host_piece) { + if (base::EndsWith(host_piece, ".")) + host_piece.remove_suffix(1); + return host_piece; +} + +} // namespace + +// static +bool URLPattern::IsValidSchemeForExtensions(base::StringPiece scheme) { + for (size_t i = 0; i < base::size(kValidSchemes); ++i) { + if (scheme == kValidSchemes[i]) + return true; + } + return false; +} + +// static +int URLPattern::GetValidSchemeMaskForExtensions() { + int result = 0; + for (size_t i = 0; i < base::size(kValidSchemeMasks); ++i) + result |= kValidSchemeMasks[i]; + return result; +} + +URLPattern::URLPattern() + : valid_schemes_(SCHEME_NONE), + match_all_urls_(false), + match_subdomains_(false), + port_("*") {} + +URLPattern::URLPattern(int valid_schemes) + : valid_schemes_(valid_schemes), + match_all_urls_(false), + match_subdomains_(false), + port_("*") {} + +URLPattern::URLPattern(int valid_schemes, base::StringPiece pattern) + // Strict error checking is used, because this constructor is only + // appropriate when we know |pattern| is valid. + : valid_schemes_(valid_schemes), + match_all_urls_(false), + match_subdomains_(false), + port_("*") { + ParseResult result = Parse(pattern); + DCHECK_EQ(ParseResult::kSuccess, result) + << "Parsing unexpectedly failed for pattern: " << pattern << ": " + << GetParseResultString(result); +} + +URLPattern::URLPattern(const URLPattern& other) = default; + +URLPattern::URLPattern(URLPattern&& other) = default; + +URLPattern::~URLPattern() { +} + +URLPattern& URLPattern::operator=(const URLPattern& other) = default; + +URLPattern& URLPattern::operator=(URLPattern&& other) = default; + +bool URLPattern::operator<(const URLPattern& other) const { + return GetAsString() < other.GetAsString(); +} + +bool URLPattern::operator>(const URLPattern& other) const { + return GetAsString() > other.GetAsString(); +} + +bool URLPattern::operator==(const URLPattern& other) const { + return GetAsString() == other.GetAsString(); +} + +std::ostream& operator<<(std::ostream& out, const URLPattern& url_pattern) { + return out << '"' << url_pattern.GetAsString() << '"'; +} + +URLPattern::ParseResult URLPattern::Parse(base::StringPiece pattern) { + spec_.clear(); + SetMatchAllURLs(false); + SetMatchSubdomains(false); + SetPort("*"); + + // Special case pattern to match every valid URL. + if (pattern == kAllUrlsPattern) { + SetMatchAllURLs(true); + return ParseResult::kSuccess; + } + + // Parse out the scheme. + size_t scheme_end_pos = pattern.find(url::kStandardSchemeSeparator); + bool has_standard_scheme_separator = true; + + // Some urls also use ':' alone as the scheme separator. + if (scheme_end_pos == base::StringPiece::npos) { + scheme_end_pos = pattern.find(':'); + has_standard_scheme_separator = false; + } + + if (scheme_end_pos == base::StringPiece::npos) + return ParseResult::kMissingSchemeSeparator; + + if (!SetScheme(pattern.substr(0, scheme_end_pos))) + return ParseResult::kInvalidScheme; + + bool standard_scheme = IsStandardScheme(scheme_); + if (standard_scheme != has_standard_scheme_separator) + return ParseResult::kWrongSchemeSeparator; + + // Advance past the scheme separator. + scheme_end_pos += + (standard_scheme ? strlen(url::kStandardSchemeSeparator) : 1); + if (scheme_end_pos >= pattern.size()) + return ParseResult::kEmptyHost; + + // Parse out the host and path. + size_t host_start_pos = scheme_end_pos; + size_t path_start_pos = 0; + + if (!standard_scheme) { + path_start_pos = host_start_pos; + } else if (scheme_ == url::kFileScheme) { + size_t host_end_pos = pattern.find(kPathSeparator, host_start_pos); + if (host_end_pos == base::StringPiece::npos) { + // Allow hostname omission. + // e.g. file://* is interpreted as file:///*, + // file://foo* is interpreted as file:///foo*. + path_start_pos = host_start_pos - 1; + } else { + // Ignore hostname if scheme is file://. + // e.g. file://localhost/foo is equal to file:///foo. + path_start_pos = host_end_pos; + } + } else { + size_t host_end_pos = pattern.find(kPathSeparator, host_start_pos); + + // Host is required. + if (host_start_pos == host_end_pos) + return ParseResult::kEmptyHost; + + if (host_end_pos == base::StringPiece::npos) + return ParseResult::kEmptyPath; + + base::StringPiece host_and_port = + pattern.substr(host_start_pos, host_end_pos - host_start_pos); + + size_t port_separator_pos = base::StringPiece::npos; + if (host_and_port[0] != '[') { + // Not IPv6 (either IPv4 or just a normal address). + port_separator_pos = host_and_port.find(':'); + } else { // IPv6. + size_t host_end_pos = host_and_port.find(']'); + if (host_end_pos == base::StringPiece::npos) + return ParseResult::kInvalidHost; + if (host_end_pos == 1) + return ParseResult::kEmptyHost; + + if (host_end_pos < host_and_port.length() - 1) { + // The host isn't the only component. Check for a port. This would + // require a ':' to follow the closing ']' from the host. + if (host_and_port[host_end_pos + 1] != ':') + return ParseResult::kInvalidHost; + + port_separator_pos = host_end_pos + 1; + } + } + + if (port_separator_pos != base::StringPiece::npos && + !SetPort(host_and_port.substr(port_separator_pos + 1))) { + return ParseResult::kInvalidPort; + } + + // Note: this substr() will be the entire string if the port position + // wasn't found. + base::StringPiece host_piece = host_and_port.substr(0, port_separator_pos); + + if (host_piece.empty()) + return ParseResult::kEmptyHost; + + if (host_piece == "*") { + match_subdomains_ = true; + host_piece = base::StringPiece(); + } else if (base::StartsWith(host_piece, "*.")) { + if (host_piece.length() == 2) { + // We don't allow just '*.' as a host. + return ParseResult::kEmptyHost; + } + match_subdomains_ = true; + host_piece = host_piece.substr(2); + } + + host_ = std::string(host_piece); + + path_start_pos = host_end_pos; + } + + SetPath(pattern.substr(path_start_pos)); + + // No other '*' can occur in the host, though. This isn't necessary, but is + // done as a convenience to developers who might otherwise be confused and + // think '*' works as a glob in the host. + if (host_.find('*') != std::string::npos) + return ParseResult::kInvalidHostWildcard; + + if (!host_.empty()) { + // If |host_| is present (i.e., isn't a wildcard), we need to canonicalize + // it. + url::CanonHostInfo host_info; + host_ = net::CanonicalizeHost(host_, &host_info); + // net::CanonicalizeHost() returns an empty string on failure. + if (host_.empty()) + return ParseResult::kInvalidHost; + } + + // Null characters are not allowed in hosts. + if (host_.find('\0') != std::string::npos) + return ParseResult::kInvalidHost; + + return ParseResult::kSuccess; +} + +void URLPattern::SetValidSchemes(int valid_schemes) { + // TODO(devlin): Should we check that valid_schemes agrees with |scheme_| + // here? Otherwise, valid_schemes_ and schemes_ may stop agreeing with each + // other (e.g., in the case of `*://*/*`, where the scheme should only be + // http or https). + spec_.clear(); + valid_schemes_ = valid_schemes; +} + +void URLPattern::SetHost(base::StringPiece host) { + spec_.clear(); + host_.assign(host.data(), host.size()); +} + +void URLPattern::SetMatchAllURLs(bool val) { + spec_.clear(); + match_all_urls_ = val; + + if (val) { + match_subdomains_ = true; + scheme_ = "*"; + host_.clear(); + SetPath("/*"); + } +} + +void URLPattern::SetMatchSubdomains(bool val) { + spec_.clear(); + match_subdomains_ = val; +} + +bool URLPattern::SetScheme(base::StringPiece scheme) { + spec_.clear(); + scheme_.assign(scheme.data(), scheme.size()); + if (scheme_ == "*") { + valid_schemes_ &= (SCHEME_HTTP | SCHEME_HTTPS); + } else if (!IsValidScheme(scheme_)) { + return false; + } + return true; +} + +bool URLPattern::IsValidScheme(base::StringPiece scheme) const { + if (valid_schemes_ == SCHEME_ALL) + return true; + + for (size_t i = 0; i < base::size(kValidSchemes); ++i) { + if (scheme == kValidSchemes[i] && (valid_schemes_ & kValidSchemeMasks[i])) + return true; + } + + return false; +} + +void URLPattern::SetPath(base::StringPiece path) { + spec_.clear(); + path_.assign(path.data(), path.size()); + path_escaped_ = path_; + base::ReplaceSubstringsAfterOffset(&path_escaped_, 0, "\\", "\\\\"); + base::ReplaceSubstringsAfterOffset(&path_escaped_, 0, "?", "\\?"); +} + +bool URLPattern::SetPort(base::StringPiece port) { + spec_.clear(); + if (IsValidPortForScheme(scheme_, port)) { + port_.assign(port.data(), port.size()); + return true; + } + return false; +} + +bool URLPattern::MatchesURL(const GURL& test) const { + // Invalid URLs can never match. + if (!test.is_valid()) + return false; + + const GURL* test_url = &test; + bool has_inner_url = test.inner_url() != nullptr; + + if (has_inner_url) { + if (!test.SchemeIsFileSystem()) + return false; // The only nested URLs we handle are filesystem URLs. + test_url = test.inner_url(); + } + + // Ensure the scheme matches first, since may not match this URL if + // the scheme is excluded. + if (!MatchesScheme(test_url->scheme_piece())) + return false; + + if (match_all_urls_) + return true; + + // Unless |match_all_urls_| is true, the grammar only permits matching + // URLs with nonempty paths. + if (!test.has_path()) + return false; + + std::string path_for_request = test.PathForRequest(); + if (has_inner_url) { + path_for_request = base::StringPrintf("%s%s", test_url->path_piece().data(), + path_for_request.c_str()); + } + + return MatchesSecurityOriginHelper(*test_url) && + MatchesPath(path_for_request); +} + +bool URLPattern::MatchesSecurityOrigin(const GURL& test) const { + const GURL* test_url = &test; + bool has_inner_url = test.inner_url() != NULL; + + if (has_inner_url) { + if (!test.SchemeIsFileSystem()) + return false; // The only nested URLs we handle are filesystem URLs. + test_url = test.inner_url(); + } + + if (!MatchesScheme(test_url->scheme())) + return false; + + if (match_all_urls_) + return true; + + return MatchesSecurityOriginHelper(*test_url); +} + +bool URLPattern::MatchesScheme(base::StringPiece test) const { + if (!IsValidScheme(test)) + return false; + + return scheme_ == "*" || test == scheme_; +} + +bool URLPattern::MatchesHost(base::StringPiece host) const { + // TODO(devlin): This is a bit sad. Parsing urls is expensive. However, it's + // important that we do this conversion to a GURL in order to canonicalize the + // host (the pattern's host_ already is canonicalized from Parse()). We can't + // just do string comparison. + return MatchesHost( + GURL(base::StringPrintf("%s%s%s/", url::kHttpScheme, + url::kStandardSchemeSeparator, host.data()))); +} + +bool URLPattern::MatchesHost(const GURL& test) const { + base::StringPiece test_host(CanonicalizeHostForMatching(test.host_piece())); + const base::StringPiece pattern_host(CanonicalizeHostForMatching(host_)); + + // If the hosts are exactly equal, we have a match. + if (test_host == pattern_host) + return true; + + // If we're matching subdomains, and we have no host in the match pattern, + // that means that we're matching all hosts, which means we have a match no + // matter what the test host is. + if (match_subdomains_ && pattern_host.empty()) + return true; + + // Otherwise, we can only match if our match pattern matches subdomains. + if (!match_subdomains_) + return false; + + // We don't do subdomain matching against IP addresses, so we can give up now + // if the test host is an IP address. + if (test.HostIsIPAddress()) + return false; + + // Check if the test host is a subdomain of our host. + if (test_host.length() <= (pattern_host.length() + 1)) + return false; + + if (!base::EndsWith(test_host, pattern_host)) + return false; + + return test_host[test_host.length() - pattern_host.length() - 1] == '.'; +} + +bool URLPattern::MatchesEffectiveTld( + net::registry_controlled_domains::PrivateRegistryFilter private_filter, + net::registry_controlled_domains::UnknownRegistryFilter unknown_filter) + const { + // Check if it matches all urls or is a pattern like http://*/*. + if (match_all_urls_ || (match_subdomains_ && host_.empty())) + return true; + + // If this doesn't even match subdomains, it can't possibly be a TLD wildcard. + if (!match_subdomains_) + return false; + + // If there was more than just a TLD in the host (e.g., *.foobar.com), it + // doesn't match all hosts in an effective TLD. + if (net::registry_controlled_domains::HostHasRegistryControlledDomain( + host_, unknown_filter, private_filter)) { + return false; + } + + // At this point the host could either be just a TLD ("com") or some unknown + // TLD-like string ("notatld"). To disambiguate between them construct a + // fake URL, and check the registry. + // + // If we recognized this TLD, then this is a pattern like *.com, and it + // matches an effective TLD. + return net::registry_controlled_domains::HostHasRegistryControlledDomain( + "notatld." + host_, unknown_filter, private_filter); +} + +bool URLPattern::MatchesSingleOrigin() const { + // Strictly speaking, the port is part of the origin, but in URLPattern it + // defaults to *. It's not very interesting anyway, so leave it out. + return !MatchesEffectiveTld() && scheme_ != "*" && !match_subdomains_; +} + +bool URLPattern::MatchesPath(base::StringPiece test) const { + // Make the behaviour of OverlapsWith consistent with MatchesURL, which is + // need to match hosted apps on e.g. 'google.com' also run on 'google.com/'. + // The below if is a no-copy way of doing (test + "/*" == path_escaped_). + if (path_escaped_.length() == test.length() + 2 && + base::StartsWith(path_escaped_.c_str(), test) && + base::EndsWith(path_escaped_, "/*")) { + return true; + } + + return base::MatchPattern(test, path_escaped_); +} + +const std::string& URLPattern::GetAsString() const { + if (!spec_.empty()) + return spec_; + + if (match_all_urls_) { + spec_ = kAllUrlsPattern; + return spec_; + } + + bool standard_scheme = IsStandardScheme(scheme_); + + std::string spec = scheme_ + + (standard_scheme ? url::kStandardSchemeSeparator : ":"); + + if (scheme_ != url::kFileScheme && standard_scheme) { + if (match_subdomains_) { + spec += "*"; + if (!host_.empty()) + spec += "."; + } + + if (!host_.empty()) + spec += host_; + + if (port_ != "*") { + spec += ":"; + spec += port_; + } + } + + if (!path_.empty()) + spec += path_; + + spec_ = std::move(spec); + return spec_; +} + +bool URLPattern::OverlapsWith(const URLPattern& other) const { + if (match_all_urls() || other.match_all_urls()) + return true; + return (MatchesAnyScheme(other.GetExplicitSchemes()) || + other.MatchesAnyScheme(GetExplicitSchemes())) + && (MatchesHost(other.host()) || other.MatchesHost(host())) + && (MatchesPortPattern(other.port()) || other.MatchesPortPattern(port())) + && (MatchesPath(StripTrailingWildcard(other.path())) || + other.MatchesPath(StripTrailingWildcard(path()))); +} + +bool URLPattern::Contains(const URLPattern& other) const { + // Important: it's not enough to just check match_all_urls(); we also need to + // make sure that the schemes in this pattern are a superset of those in + // |other|. + if (match_all_urls() && + (valid_schemes_ & other.valid_schemes_) == other.valid_schemes_) { + return true; + } + + return MatchesAllSchemes(other.GetExplicitSchemes()) && + MatchesHost(other.host()) && + (!other.match_subdomains_ || match_subdomains_) && + MatchesPortPattern(other.port()) && + MatchesPath(StripTrailingWildcard(other.path())); +} + +absl::optional URLPattern::CreateIntersection( + const URLPattern& other) const { + // Easy case: Schemes don't overlap. Return nullopt. + int intersection_schemes = URLPattern::SCHEME_NONE; + if (valid_schemes_ == URLPattern::SCHEME_ALL) + intersection_schemes = other.valid_schemes_; + else if (other.valid_schemes_ == URLPattern::SCHEME_ALL) + intersection_schemes = valid_schemes_; + else + intersection_schemes = valid_schemes_ & other.valid_schemes_; + + if (intersection_schemes == URLPattern::SCHEME_NONE) + return absl::nullopt; + + { + // In a few cases, we can (mostly) return a copy of one of the patterns. + // This can happen when either: + // - The URLPattern's are identical (possibly excluding valid_schemes_) + // - One of the patterns has match_all_urls() equal to true. + // NOTE(devlin): Theoretically, we could use Contains() instead of + // match_all_urls() here. However, Contains() strips the trailing wildcard + // from the path, which could yield the incorrect result. + const URLPattern* copy_source = nullptr; + if (*this == other || other.match_all_urls()) + copy_source = this; + else if (match_all_urls()) + copy_source = &other; + + if (copy_source) { + // NOTE: equality checks don't take into account valid_schemes_, and + // schemes can be different in the case of match_all_urls() as well, so + // we can't always just return *copy_source. + if (intersection_schemes == copy_source->valid_schemes_) + return *copy_source; + URLPattern result(intersection_schemes); + ParseResult parse_result = result.Parse(copy_source->GetAsString()); + CHECK_EQ(ParseResult::kSuccess, parse_result); + return result; + } + } + + // No more easy cases. Go through component by component to find the patterns + // that intersect. + + // Note: Alias the function type (rather than using auto) because + // MatchesHost() is overloaded. + using match_function_type = bool (URLPattern::*)(base::StringPiece) const; + + auto get_intersection = [this, &other](base::StringPiece own_str, + base::StringPiece other_str, + match_function_type match_function, + base::StringPiece* out) { + if ((this->*match_function)(other_str)) { + *out = other_str; + return true; + } + if ((other.*match_function)(own_str)) { + *out = own_str; + return true; + } + return false; + }; + + base::StringPiece scheme; + base::StringPiece host; + base::StringPiece port; + base::StringPiece path; + // If any pieces fail to overlap, then there is no intersection. + if (!get_intersection(scheme_, other.scheme_, &URLPattern::MatchesScheme, + &scheme) || + !get_intersection(host_, other.host_, &URLPattern::MatchesHost, &host) || + !get_intersection(port_, other.port_, &URLPattern::MatchesPortPattern, + &port) || + !get_intersection(path_, other.path_, &URLPattern::MatchesPath, &path)) { + return absl::nullopt; + } + + // Only match subdomains if both patterns match subdomains. + base::StringPiece subdomains; + if (match_subdomains_ && other.match_subdomains_) { + // The host may be empty (e.g., in the case of *://*/* - in that case, only + // append '*' instead of '*.'. + subdomains = host.empty() ? "*" : "*."; + } + + base::StringPiece scheme_separator = + IsStandardScheme(scheme) ? url::kStandardSchemeSeparator : ":"; + + std::string pattern_str = base::StrCat( + {scheme, scheme_separator, subdomains, host, ":", port, path}); + + URLPattern pattern(intersection_schemes); + ParseResult result = pattern.Parse(pattern_str); + // TODO(devlin): I don't think there's any way this should ever fail, but + // use a CHECK() to flush any cases out. If nothing crops up, downgrade this + // to a DCHECK in M72. + CHECK_EQ(ParseResult::kSuccess, result); + + return pattern; +} + +bool URLPattern::MatchesAnyScheme( + const std::vector& schemes) const { + for (auto i = schemes.cbegin(); i != schemes.cend(); ++i) { + if (MatchesScheme(*i)) + return true; + } + + return false; +} + +bool URLPattern::MatchesAllSchemes( + const std::vector& schemes) const { + for (auto i = schemes.cbegin(); i != schemes.cend(); ++i) { + if (!MatchesScheme(*i)) + return false; + } + + return true; +} + +bool URLPattern::MatchesSecurityOriginHelper(const GURL& test) const { + // Ignore hostname if scheme is file://. + if (scheme_ != url::kFileScheme && !MatchesHost(test)) + return false; + + if (!MatchesPortPattern(base::NumberToString(test.EffectiveIntPort()))) + return false; + + return true; +} + +bool URLPattern::MatchesPortPattern(base::StringPiece port) const { + return port_ == "*" || port_ == port; +} + +std::vector URLPattern::GetExplicitSchemes() const { + std::vector result; + + if (scheme_ != "*" && !match_all_urls_ && IsValidScheme(scheme_)) { + result.push_back(scheme_); + return result; + } + + for (size_t i = 0; i < base::size(kValidSchemes); ++i) { + if (MatchesScheme(kValidSchemes[i])) { + result.push_back(kValidSchemes[i]); + } + } + + return result; +} + +std::vector URLPattern::ConvertToExplicitSchemes() const { + std::vector explicit_schemes = GetExplicitSchemes(); + std::vector result; + + for (std::vector::const_iterator i = explicit_schemes.begin(); + i != explicit_schemes.end(); ++i) { + URLPattern temp = *this; + temp.SetScheme(*i); + temp.SetMatchAllURLs(false); + result.push_back(temp); + } + + return result; +} + +// static +const char* URLPattern::GetParseResultString( + URLPattern::ParseResult parse_result) { + return kParseResultMessages[static_cast(parse_result)]; +} diff --git a/components/user_scripts/common/url_pattern.h b/components/user_scripts/common/url_pattern.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/url_pattern.h @@ -0,0 +1,302 @@ +// 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. +#ifndef USERSCRIPTS_COMMON_URL_PATTERN_H_ +#define USERSCRIPTS_COMMON_URL_PATTERN_H_ + +#include +#include +#include +#include + +#include "base/strings/string_piece.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" + +class GURL; + +// A pattern that can be used to match URLs. A URLPattern is a very restricted +// subset of URL syntax: +// +// := :// | '' +// := '*' | 'http' | 'https' | 'file' | 'ftp' | 'chrome' | +// 'chrome-extension' | 'filesystem' +// := '*' | | [] | +// '*.' + +// := [':' ('*' | )] +// := '/' +// +// * Host is not used when the scheme is 'file'. +// * The path can have embedded '*' characters which act as glob wildcards. +// * '' is a special pattern that matches any valid URL that contains +// a valid scheme (as specified by valid_schemes_). +// * The '*' scheme pattern excludes file URLs. +// +// Examples of valid patterns: +// - http://*/* +// - http://*/foo* +// - https://*.google.com/foo*bar +// - file://monkey* +// - http://127.0.0.1/* +// - http://[2607:f8b0:4005:805::200e]/* +// +// Examples of invalid patterns: +// - http://* -- path not specified +// - http://*foo/bar -- * not allowed as substring of host component +// - http://foo.*.bar/baz -- * must be first component +// - http:/bar -- scheme separator not found +// - foo://* -- invalid scheme +// - chrome:// -- we don't support chrome internal URLs +class URLPattern { + public: + // A collection of scheme bitmasks for use with valid_schemes. + enum SchemeMasks { + SCHEME_NONE = 0, + SCHEME_HTTP = 1 << 0, + SCHEME_HTTPS = 1 << 1, + SCHEME_FILE = 1 << 2, + SCHEME_FTP = 1 << 3, + SCHEME_CHROMEUI = 1 << 4, + SCHEME_EXTENSION = 1 << 5, + SCHEME_FILESYSTEM = 1 << 6, + SCHEME_WS = 1 << 7, + SCHEME_WSS = 1 << 8, + SCHEME_DATA = 1 << 9, + SCHEME_URN = 1 << 10, + + // IMPORTANT! + // SCHEME_ALL will match every scheme, including chrome://, chrome- + // extension://, about:, etc. Because this has lots of security + // implications, third-party extensions should usually not be able to get + // access to URL patterns initialized this way. If there is a reason + // for violating this general rule, document why this it safe. + SCHEME_ALL = -1, + }; + + // Error codes returned from Parse(). + enum class ParseResult { + kSuccess = 0, + kMissingSchemeSeparator, + kInvalidScheme, + kWrongSchemeSeparator, + kEmptyHost, + kInvalidHostWildcard, + kEmptyPath, + kInvalidPort, + kInvalidHost, + kNumParseResults, + }; + + // The string pattern. + static const char kAllUrlsPattern[]; + + // Returns true if the given |scheme| is considered valid for extensions. + static bool IsValidSchemeForExtensions(base::StringPiece scheme); + + // Returns the mask for all schemes considered valid for extensions. + static int GetValidSchemeMaskForExtensions(); + + explicit URLPattern(int valid_schemes); + + // Convenience to construct a URLPattern from a string. If the string is not + // known ahead of time, use Parse() instead, which returns success or failure. + URLPattern(int valid_schemes, base::StringPiece pattern); + + URLPattern(); + URLPattern(const URLPattern& other); + URLPattern(URLPattern&& other); + ~URLPattern(); + + URLPattern& operator=(const URLPattern& other); + URLPattern& operator=(URLPattern&& other); + + bool operator<(const URLPattern& other) const; + bool operator>(const URLPattern& other) const; + bool operator==(const URLPattern& other) const; + + // Initializes this instance by parsing the provided string. Returns + // URLPattern::ParseResult::kSuccess on success, or an error code otherwise. + // On failure, this instance will have some intermediate values and is in an + // invalid state. + ParseResult Parse(base::StringPiece pattern_str); + + // Gets the bitmask of valid schemes. + int valid_schemes() const { return valid_schemes_; } + void SetValidSchemes(int valid_schemes); + + // Gets the host the pattern matches. This can be an empty string if the + // pattern matches all hosts (the input was ://*/). + const std::string& host() const { return host_; } + void SetHost(base::StringPiece host); + + // Gets whether to match subdomains of host(). + bool match_subdomains() const { return match_subdomains_; } + void SetMatchSubdomains(bool val); + + // Gets the path the pattern matches with the leading slash. This can have + // embedded asterisks which are interpreted using glob rules. + const std::string& path() const { return path_; } + void SetPath(base::StringPiece path); + + // Returns true if this pattern matches all (valid) urls. + bool match_all_urls() const { return match_all_urls_; } + void SetMatchAllURLs(bool val); + + // Sets the scheme for pattern matches. This can be a single '*' if the + // pattern matches all valid schemes (as defined by the valid_schemes_ + // property). Returns false on failure (if the scheme is not valid). + bool SetScheme(base::StringPiece scheme); + // Note: You should use MatchesScheme() instead of this getter unless you + // absolutely need the exact scheme. This is exposed for testing. + const std::string& scheme() const { return scheme_; } + + // Returns true if the specified scheme can be used in this URL pattern, and + // false otherwise. Uses valid_schemes_ to determine validity. + bool IsValidScheme(base::StringPiece scheme) const; + + // Returns true if this instance matches the specified URL. Always returns + // false for invalid URLs. + bool MatchesURL(const GURL& test) const; + + // Returns true if this instance matches the specified security origin. + bool MatchesSecurityOrigin(const GURL& test) const; + + // Returns true if |test| matches our scheme. + // Note that if test is "filesystem", this may fail whereas MatchesURL + // may succeed. MatchesURL is smart enough to look at the inner_url instead + // of the outer "filesystem:" part. + bool MatchesScheme(base::StringPiece test) const; + + // Returns true if |test| matches our host. + bool MatchesHost(base::StringPiece test) const; + bool MatchesHost(const GURL& test) const; + + // Returns true if |test| matches our path. + bool MatchesPath(base::StringPiece test) const; + + // Returns true if the pattern matches all patterns in an (e)TLD. This + // includes patterns like *://*.com/*, *://*.co.uk/*, etc. A pattern that + // matches all domains (e.g., *://*/*) will return true. + // |private_filter| specifies whether private registries (like appspot.com) + // should be considered; if included, patterns like *://*.appspot.com/* will + // return true. By default, we exclude private registries (so *.appspot.com + // returns false). + // Note: This is an expensive method, and should be used sparingly! + // You should probably use URLPatternSet::ShouldWarnAllHosts(), which is + // cached. + bool MatchesEffectiveTld( + net::registry_controlled_domains::PrivateRegistryFilter private_filter = + net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES, + net::registry_controlled_domains::UnknownRegistryFilter unknown_filter = + net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES) const; + + // Returns true if the pattern only matches a single origin. The pattern may + // include a path. + bool MatchesSingleOrigin() const; + + // Sets the port. Returns false if the port is invalid. + bool SetPort(base::StringPiece port); + const std::string& port() const { return port_; } + + // Returns a string representing this instance. + const std::string& GetAsString() const; + + // Determines whether there is a URL that would match this instance and + // another instance. This method is symmetrical: Calling + // other.OverlapsWith(this) would result in the same answer. + bool OverlapsWith(const URLPattern& other) const; + + // Returns true if this pattern matches all possible URLs that |other| can + // match. For example, http://*.google.com encompasses http://www.google.com. + bool Contains(const URLPattern& other) const; + + // Creates a new URLPattern that represents the intersection of this + // URLPattern with the |other|, or base::nullopt if no intersection exists. + // For instance, given the patterns http://*.google.com/* and + // *://maps.google.com/*, the intersection is http://maps.google.com/*. + // NOTES: + // - Though scheme intersections are supported, the serialization of + // URLPatternSet does not record them. Be sure that this is safe for your + // use cases. + // - Path intersection is done on a best-effort basis. If one path clearly + // contains another, it will be handled correctly, but this method does not + // deal with cases like /*a* and /*b* (where technically the intersection + // is /*a*b*|/*b*a*); the intersection returned for that case will be empty. + absl::optional CreateIntersection(const URLPattern& other) const; + + // Converts this URLPattern into an equivalent set of URLPatterns that don't + // use a wildcard in the scheme component. If this URLPattern doesn't use a + // wildcard scheme, then the returned set will contain one element that is + // equivalent to this instance. + std::vector ConvertToExplicitSchemes() const; + + static bool EffectiveHostCompare(const URLPattern& a, const URLPattern& b) { + if (a.match_all_urls_ && b.match_all_urls_) + return false; + return a.host_.compare(b.host_) < 0; + } + + // Used for origin comparisons in a std::set. + class EffectiveHostCompareFunctor { + public: + bool operator()(const URLPattern& a, const URLPattern& b) const { + return EffectiveHostCompare(a, b); + } + }; + + // Get an error string for a ParseResult. + static const char* GetParseResultString(URLPattern::ParseResult parse_result); + + private: + // Returns true if any of the |schemes| items matches our scheme. + bool MatchesAnyScheme(const std::vector& schemes) const; + + // Returns true if all of the |schemes| items matches our scheme. + bool MatchesAllSchemes(const std::vector& schemes) const; + + bool MatchesSecurityOriginHelper(const GURL& test) const; + + // Returns true if our port matches the |port| pattern (it may be "*"). + bool MatchesPortPattern(base::StringPiece port) const; + + // If the URLPattern contains a wildcard scheme, returns a list of + // equivalent literal schemes, otherwise returns the current scheme. + std::vector GetExplicitSchemes() const; + + // A bitmask containing the schemes which are considered valid for this + // pattern. Parse() uses this to decide whether a pattern contains a valid + // scheme. + int valid_schemes_; + + // True if this is a special-case "" pattern. + bool match_all_urls_; + + // The scheme for the pattern. + std::string scheme_; + + // The host without any leading "*" components. + std::string host_; + + // Whether we should match subdomains of the host. This is true if the first + // component of the pattern's host was "*". + bool match_subdomains_; + + // The port. + std::string port_; + + // The path to match. This is everything after the host of the URL, or + // everything after the scheme in the case of file:// URLs. + std::string path_; + + // The path with "?" and "\" characters escaped for use with the + // MatchPattern() function. + std::string path_escaped_; + + // A string representing this URLPattern. + mutable std::string spec_; +}; + +std::ostream& operator<<(std::ostream& out, const URLPattern& url_pattern); + +typedef std::vector URLPatternList; + +#endif // USERSCRIPTS_COMMON_URL_PATTERN_H_ diff --git a/components/user_scripts/common/url_pattern_set.cc b/components/user_scripts/common/url_pattern_set.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/url_pattern_set.cc @@ -0,0 +1,334 @@ +// 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. + +#include "url_pattern_set.h" + +#include +#include + +#include "base/containers/contains.h" +#include "base/logging.h" +#include "base/stl_util.h" +#include "base/values.h" +#include "error_utils.h" +#include "url_pattern.h" +#include "url/gurl.h" +#include "url/origin.h" +#include "url/url_constants.h" +#include "user_scripts_features.h" + +namespace user_scripts { + +namespace { + +const char kInvalidURLPatternError[] = "Invalid url pattern '*'"; + +} // namespace + +// static +URLPatternSet URLPatternSet::CreateDifference(const URLPatternSet& set1, + const URLPatternSet& set2) { + return URLPatternSet(base::STLSetDifference>( + set1.patterns_, set2.patterns_)); +} + +// static +URLPatternSet URLPatternSet::CreateIntersection( + const URLPatternSet& set1, + const URLPatternSet& set2, + IntersectionBehavior intersection_behavior) { + // Note: leverage return value optimization; always return the same object. + URLPatternSet result; + + if (intersection_behavior == IntersectionBehavior::kStringComparison) { + // String comparison just relies on STL set behavior, which looks at the + // string representation. + result = URLPatternSet(base::STLSetIntersection>( + set1.patterns_, set2.patterns_)); + return result; + } + + // Look for a semantic intersection. + + // Step 1: Iterate over each set. Find any patterns that are completely + // contained by the other (thus being necessarily present in any intersection) + // and add them, collecting the others in a set of unique items. + // Note: Use a collection of pointers for the uniques to avoid excessive + // copies. Since these are owned by the URLPatternSet passed in, which is + // const, this should be safe. + std::vector unique_set1; + for (const URLPattern& pattern : set1) { + if (set2.ContainsPattern(pattern)) + result.patterns_.insert(pattern); + else + unique_set1.push_back(&pattern); + } + std::vector unique_set2; + for (const URLPattern& pattern : set2) { + if (set1.ContainsPattern(pattern)) + result.patterns_.insert(pattern); + else + unique_set2.push_back(&pattern); + } + + // If we're just looking for patterns contained by both, we're done. + if (intersection_behavior == IntersectionBehavior::kPatternsContainedByBoth) + return result; + + DCHECK_EQ(IntersectionBehavior::kDetailed, intersection_behavior); + + // Step 2: Iterate over all the unique patterns and find the intersections + // they have with the other patterns. + for (const auto* pattern : unique_set1) { + for (const auto* pattern2 : unique_set2) { + absl::optional intersection = + pattern->CreateIntersection(*pattern2); + if (intersection) + result.patterns_.insert(std::move(*intersection)); + } + } + + return result; +} + +// static +URLPatternSet URLPatternSet::CreateUnion(const URLPatternSet& set1, + const URLPatternSet& set2) { + return URLPatternSet( + base::STLSetUnion>(set1.patterns_, set2.patterns_)); +} + +// static +URLPatternSet URLPatternSet::CreateUnion( + const std::vector& sets) { + URLPatternSet result; + if (sets.empty()) + return result; + + // N-way union algorithm is basic O(nlog(n)) merge algorithm. + // + // Do the first merge step into a working set so that we don't mutate any of + // the input. + // TODO(devlin): Looks like this creates a bunch of copies; we can probably + // clean that up. + std::vector working; + for (size_t i = 0; i < sets.size(); i += 2) { + if (i + 1 < sets.size()) + working.push_back(CreateUnion(sets[i], sets[i + 1])); + else + working.push_back(sets[i].Clone()); + } + + for (size_t skip = 1; skip < working.size(); skip *= 2) { + for (size_t i = 0; i < (working.size() - skip); i += skip) { + URLPatternSet u = CreateUnion(working[i], working[i + skip]); + working[i].patterns_.swap(u.patterns_); + } + } + + result.patterns_.swap(working[0].patterns_); + return result; +} + +URLPatternSet::URLPatternSet() = default; + +URLPatternSet::URLPatternSet(URLPatternSet&& rhs) = default; + +URLPatternSet::URLPatternSet(const std::set& patterns) + : patterns_(patterns) {} + +URLPatternSet::~URLPatternSet() = default; + +URLPatternSet& URLPatternSet::operator=(URLPatternSet&& rhs) = default; + +bool URLPatternSet::operator==(const URLPatternSet& other) const { + return patterns_ == other.patterns_; +} + +std::ostream& operator<<(std::ostream& out, + const URLPatternSet& url_pattern_set) { + out << "{ "; + + auto iter = url_pattern_set.patterns().cbegin(); + if (!url_pattern_set.patterns().empty()) { + out << *iter; + ++iter; + } + + for (;iter != url_pattern_set.patterns().end(); ++iter) + out << ", " << *iter; + + if (!url_pattern_set.patterns().empty()) + out << " "; + + out << "}"; + return out; +} + +URLPatternSet URLPatternSet::Clone() const { + return URLPatternSet(patterns_); +} + +bool URLPatternSet::is_empty() const { + return patterns_.empty(); +} + +size_t URLPatternSet::size() const { + return patterns_.size(); +} + +bool URLPatternSet::AddPattern(const URLPattern& pattern) { + return patterns_.insert(pattern).second; +} + +void URLPatternSet::AddPatterns(const URLPatternSet& set) { + patterns_.insert(set.patterns().begin(), + set.patterns().end()); +} + +void URLPatternSet::ClearPatterns() { + patterns_.clear(); +} + +bool URLPatternSet::AddOrigin(int valid_schemes, const GURL& origin) { + if (origin.is_empty()) + return false; + const url::Origin real_origin = url::Origin::Create(origin); + DCHECK(real_origin.IsSameOriginWith(url::Origin::Create(origin.GetOrigin()))); + URLPattern origin_pattern(valid_schemes); + // Origin adding could fail if |origin| does not match |valid_schemes|. + if (origin_pattern.Parse(origin.spec()) != + URLPattern::ParseResult::kSuccess) { + return false; + } + origin_pattern.SetPath("/*"); + return AddPattern(origin_pattern); +} + +bool URLPatternSet::Contains(const URLPatternSet& other) const { + for (auto it = other.begin(); it != other.end(); ++it) { + if (!ContainsPattern(*it)) + return false; + } + + return true; +} + +bool URLPatternSet::ContainsPattern(const URLPattern& pattern) const { + for (auto it = begin(); it != end(); ++it) { + if (it->Contains(pattern)) + return true; + } + return false; +} + +bool URLPatternSet::MatchesURL(const GURL& url) const { + for (auto pattern = patterns_.cbegin(); pattern != patterns_.cend(); + ++pattern) { + if (pattern->MatchesURL(url)) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: URLPatternSet::MatchesURL true " << url.spec(); + + return true; + } + } + + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: URLPatternSet::MatchesURL false " << url.spec(); + + return false; +} + +bool URLPatternSet::MatchesAllURLs() const { + for (auto host = begin(); host != end(); ++host) { + if (host->match_all_urls() || + (host->match_subdomains() && host->host().empty())) + return true; + } + return false; +} + +bool URLPatternSet::MatchesSecurityOrigin(const GURL& origin) const { + for (auto pattern = patterns_.begin(); pattern != patterns_.end(); + ++pattern) { + if (pattern->MatchesSecurityOrigin(origin)) + return true; + } + + return false; +} + +bool URLPatternSet::OverlapsWith(const URLPatternSet& other) const { + // Two extension extents overlap if there is any one URL that would match at + // least one pattern in each of the extents. + for (auto i = patterns_.cbegin(); i != patterns_.cend(); ++i) { + for (auto j = other.patterns().cbegin(); j != other.patterns().cend(); + ++j) { + if (i->OverlapsWith(*j)) + return true; + } + } + + return false; +} + +std::unique_ptr URLPatternSet::ToValue() const { + std::unique_ptr value(new base::ListValue); + for (auto i = patterns_.cbegin(); i != patterns_.cend(); ++i) { + base::Value pattern_str_value(i->GetAsString()); + if (!base::Contains(value->GetList(), pattern_str_value)) + value->Append(std::move(pattern_str_value)); + } + return value; +} + +bool URLPatternSet::Populate(const std::vector& patterns, + int valid_schemes, + bool allow_file_access, + std::string* error) { + ClearPatterns(); + for (size_t i = 0; i < patterns.size(); ++i) { + URLPattern pattern(valid_schemes); + if (pattern.Parse(patterns[i]) != URLPattern::ParseResult::kSuccess) { + if (error) { + *error = ErrorUtils::FormatErrorMessage(kInvalidURLPatternError, + patterns[i]); + } else { + LOG(ERROR) << "Invalid url pattern: " << patterns[i]; + } + return false; + } + if (!allow_file_access && pattern.MatchesScheme(url::kFileScheme)) { + pattern.SetValidSchemes( + pattern.valid_schemes() & ~URLPattern::SCHEME_FILE); + } + AddPattern(pattern); + } + return true; +} + +std::unique_ptr> URLPatternSet::ToStringVector() + const { + std::unique_ptr> value(new std::vector); + for (auto i = patterns_.cbegin(); i != patterns_.cend(); ++i) { + value->push_back(i->GetAsString()); + } + return value; +} + +bool URLPatternSet::Populate(const base::ListValue& value, + int valid_schemes, + bool allow_file_access, + std::string* error) { + std::vector patterns; + for (size_t i = 0; i < value.GetSize(); ++i) { + std::string item; + if (!value.GetString(i, &item)) + return false; + patterns.push_back(item); + } + return Populate(patterns, valid_schemes, allow_file_access, error); +} + +} // namespace extensions diff --git a/components/user_scripts/common/url_pattern_set.h b/components/user_scripts/common/url_pattern_set.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/url_pattern_set.h @@ -0,0 +1,161 @@ +// 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. + +#ifndef USERSCRIPTS_COMMON_URL_PATTERN_SET_H_ +#define USERSCRIPTS_COMMON_URL_PATTERN_SET_H_ + +#include + +#include +#include +#include + +#include "base/macros.h" +#include "url_pattern.h" + +class GURL; + +namespace base { +class ListValue; +class Value; +} + +namespace user_scripts { + +// Represents the set of URLs an extension uses for web content. +class URLPatternSet { + public: + typedef std::set::const_iterator const_iterator; + typedef std::set::iterator iterator; + + // Returns |set1| - |set2|. + static URLPatternSet CreateDifference(const URLPatternSet& set1, + const URLPatternSet& set2); + + enum class IntersectionBehavior { + // For the following descriptions, consider the two URLPatternSets: + // Set 1: {"https://example.com/*", "https://*.google.com/*", "http://*/*"} + // Set 2: {"https://example.com/*", "https://google.com/maps", + // "*://chromium.org/*"} + + // Only includes patterns that are exactly in both sets. The intersection of + // the two sets above is {"https://example.com/*"}, since that is the only + // pattern that appears exactly in each. + kStringComparison, + + // Includes patterns that are effectively contained by both sets. The + // intersection of the two sets above is + // { + // "https://example.com/*" (contained exactly by each set) + // "https://google.com/maps" (contained exactly by set 2 and a strict + // subset of https://*.google.com/* in set 1) + // } + kPatternsContainedByBoth, + + // Includes patterns that are contained by both sets and creates new + // patterns to represent the intersection of any others. The intersection of + // the two sets above is + // { + // "https://example.com/*" (contained exactly by each set) + // "https://google.com/maps" (contained exactly by set 2 and a strict + // subset of https://*.google.com/* in set 1) + // "http://chromium.org/*" (the overlap between "http://*/*" in set 1 and + // *://chromium.org/*" in set 2). + // } + // Note that this is the most computationally expensive - potentially + // O(n^2) - since it can require comparing each pattern in one set to every + // pattern in the other set. + kDetailed, + }; + + // Returns the intersection of |set1| and |set2| according to + // |intersection_behavior|. + static URLPatternSet CreateIntersection( + const URLPatternSet& set1, + const URLPatternSet& set2, + IntersectionBehavior intersection_behavior); + + // Returns the union of |set1| and |set2|. + static URLPatternSet CreateUnion(const URLPatternSet& set1, + const URLPatternSet& set2); + + // Returns the union of all sets in |sets|. + static URLPatternSet CreateUnion(const std::vector& sets); + + URLPatternSet(); + URLPatternSet(URLPatternSet&& rhs); + explicit URLPatternSet(const std::set& patterns); + ~URLPatternSet(); + + URLPatternSet& operator=(URLPatternSet&& rhs); + bool operator==(const URLPatternSet& rhs) const; + + bool is_empty() const; + size_t size() const; + const std::set& patterns() const { return patterns_; } + const_iterator begin() const { return patterns_.begin(); } + const_iterator end() const { return patterns_.end(); } + iterator erase(iterator iter) { return patterns_.erase(iter); } + + // Returns a copy of this URLPatternSet; not instrumented as a copy + // constructor to avoid accidental/unnecessary copies. + URLPatternSet Clone() const; + + // Adds a pattern to the set. Returns true if a new pattern was inserted, + // false if the pattern was already in the set. + bool AddPattern(const URLPattern& pattern); + + // Adds all patterns from |set| into this. + void AddPatterns(const URLPatternSet& set); + + void ClearPatterns(); + + // Adds a pattern based on |origin| to the set. + bool AddOrigin(int valid_schemes, const GURL& origin); + + // Returns true if every URL that matches |set| is matched by this. In other + // words, if every pattern in |set| is encompassed by a pattern in this. + bool Contains(const URLPatternSet& set) const; + + // Returns true if any pattern in this set encompasses |pattern|. + bool ContainsPattern(const URLPattern& pattern) const; + + // Test if the extent contains a URL. + bool MatchesURL(const GURL& url) const; + + // Test if the extent matches all URLs (for example, ). + bool MatchesAllURLs() const; + + bool MatchesSecurityOrigin(const GURL& origin) const; + + // Returns true if there is a single URL that would be in two extents. + bool OverlapsWith(const URLPatternSet& other) const; + + // Converts to and from Value for serialization to preferences. + std::unique_ptr ToValue() const; + bool Populate(const base::ListValue& value, + int valid_schemes, + bool allow_file_access, + std::string* error); + + // Converts to and from a vector of strings. + std::unique_ptr> ToStringVector() const; + bool Populate(const std::vector& patterns, + int valid_schemes, + bool allow_file_access, + std::string* error); + + private: + // The list of URL patterns that comprise the extent. + std::set patterns_; + + DISALLOW_COPY_AND_ASSIGN(URLPatternSet); +}; + +std::ostream& operator<<(std::ostream& out, + const URLPatternSet& url_pattern_set); + +} // namespace extensions + +#endif // USERSCRIPTS_COMMON_URL_PATTERN_SET_H_ diff --git a/components/user_scripts/common/user_script.cc b/components/user_scripts/common/user_script.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/user_script.cc @@ -0,0 +1,317 @@ +// Copyright 2013 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 "user_script.h" + +#include +#include + +#include +#include + +#include "base/atomic_sequence_num.h" +#include "base/command_line.h" +#include "base/pickle.h" +#include "base/strings/pattern.h" +#include "base/strings/string_util.h" +#include "user_scripts_features.h" + +namespace { + +// This cannot be a plain int or int64_t because we need to generate unique IDs +// from multiple threads. +base::AtomicSequenceNumber g_user_script_id_generator; + +bool UrlMatchesGlobs(const std::vector* globs, + const GURL& url) { + for (auto glob = globs->cbegin(); glob != globs->cend(); ++glob) { + if (base::MatchPattern(url.spec(), *glob)) + return true; + } + + return false; +} + +} // namespace + +namespace user_scripts { + +// The bitmask for valid user script injectable schemes used by URLPattern. +enum { + kValidUserScriptSchemes = //URLPattern::SCHEME_CHROMEUI | + URLPattern::SCHEME_HTTP | + URLPattern::SCHEME_HTTPS + //| URLPattern::SCHEME_FILE | + //URLPattern::SCHEME_FTP +}; + +// static +const char UserScript::kFileExtension[] = ".user.js"; + +// static +int UserScript::GenerateUserScriptID() { + return g_user_script_id_generator.GetNext(); +} + +bool UserScript::IsURLUserScript(const GURL& url, + const std::string& mime_type) { + return base::EndsWith(url.ExtractFileName(), kFileExtension, + base::CompareCase::INSENSITIVE_ASCII) && + mime_type != "text/html"; +} + +// static +int UserScript::ValidUserScriptSchemes(bool canExecuteScriptEverywhere) { + if (canExecuteScriptEverywhere) + return URLPattern::SCHEME_ALL; + int valid_schemes = kValidUserScriptSchemes; + // if (!base::CommandLine::ForCurrentProcess()->HasSwitch( + // switches::kExtensionsOnChromeURLs)) { + // valid_schemes &= ~URLPattern::SCHEME_CHROMEUI; + // } + return valid_schemes; +} + +UserScript::File::File(const base::FilePath& extension_root, + const base::FilePath& relative_path, + const GURL& url) + : extension_root_(extension_root), + relative_path_(relative_path), + url_(url) { +} + +UserScript::File::File() {} + +UserScript::File::File(const File& other) + : extension_root_(other.extension_root_), + relative_path_(other.relative_path_), + url_(other.url_), + external_content_(other.external_content_), + content_(other.content_), + key_(other.key_) {} + +UserScript::File::~File() {} + +UserScript::UserScript() = default; +UserScript::~UserScript() = default; + +void UserScript::add_url_pattern(const URLPattern& pattern) { + url_set_.AddPattern(pattern); +} + +void UserScript::add_exclude_url_pattern(const URLPattern& pattern) { + exclude_url_set_.AddPattern(pattern); +} + +bool UserScript::MatchesURL(const GURL& url) const { + if (!url_set_.is_empty()) { + if (!url_set_.MatchesURL(url)) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: No Match for url_set"; + return false; + } + } + + if (!exclude_url_set_.is_empty()) { + if (exclude_url_set_.MatchesURL(url)) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: No Match for exclude_url_set"; + return false; + } + } + + if (!globs_.empty()) { + if (!UrlMatchesGlobs(&globs_, url)) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: No Match for globs"; + return false; + } + } + + if (!exclude_globs_.empty()) { + if (UrlMatchesGlobs(&exclude_globs_, url)) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: No Match for exclude_globs"; + return false; + } + } + + return true; +} + +bool UserScript::MatchesDocument(const GURL& effective_document_url, + bool is_subframe) const { + if (is_subframe && !match_all_frames()) + return false; + + return MatchesURL(effective_document_url); +} + +void UserScript::File::Pickle(base::Pickle* pickle) const { + pickle->WriteString(url_.spec()); + // Do not write path. It's not needed in the renderer. + // Do not write content. It will be serialized by other means. +} + +void UserScript::File::Unpickle(const base::Pickle& pickle, + base::PickleIterator* iter) { + // Read the url from the pickle. + std::string url; + CHECK(iter->ReadString(&url)); + set_url(GURL(url)); +} + +void UserScript::Pickle(base::Pickle* pickle) const { + // Write the simple types to the pickle. + pickle->WriteInt(run_location()); + pickle->WriteInt(user_script_id_); + pickle->WriteBool(emulate_greasemonkey()); + pickle->WriteBool(match_all_frames()); + pickle->WriteInt(static_cast(match_origin_as_fallback())); + pickle->WriteBool(is_incognito_enabled()); + + PickleHostID(pickle, host_id_); + pickle->WriteInt(consumer_instance_type()); + PickleGlobs(pickle, globs_); + PickleGlobs(pickle, exclude_globs_); + PickleURLPatternSet(pickle, url_set_); + PickleURLPatternSet(pickle, exclude_url_set_); + PickleScripts(pickle, js_scripts_); + PickleScripts(pickle, css_scripts_); +} + +void UserScript::PickleGlobs(base::Pickle* pickle, + const std::vector& globs) const { + pickle->WriteUInt32(globs.size()); + for (auto glob = globs.cbegin(); glob != globs.cend(); ++glob) { + pickle->WriteString(*glob); + } +} + +void UserScript::PickleHostID(base::Pickle* pickle, + const HostID& host_id) const { + pickle->WriteInt(host_id.type()); + pickle->WriteString(host_id.id()); +} + +void UserScript::PickleURLPatternSet(base::Pickle* pickle, + const URLPatternSet& pattern_list) const { + pickle->WriteUInt32(pattern_list.patterns().size()); + for (auto pattern = pattern_list.begin(); pattern != pattern_list.end(); + ++pattern) { + pickle->WriteInt(pattern->valid_schemes()); + pickle->WriteString(pattern->GetAsString()); + } +} + +void UserScript::PickleScripts(base::Pickle* pickle, + const FileList& scripts) const { + pickle->WriteUInt32(scripts.size()); + for (const std::unique_ptr& file : scripts) + file->Pickle(pickle); +} + +void UserScript::Unpickle(const base::Pickle& pickle, + base::PickleIterator* iter) { + // Read the run location. + int run_location = 0; + CHECK(iter->ReadInt(&run_location)); + CHECK(run_location >= 0 && run_location < RUN_LOCATION_LAST); + run_location_ = static_cast(run_location); + + CHECK(iter->ReadInt(&user_script_id_)); + CHECK(iter->ReadBool(&emulate_greasemonkey_)); + CHECK(iter->ReadBool(&match_all_frames_)); + int match_origin_as_fallback_int = 0; + CHECK(iter->ReadInt(&match_origin_as_fallback_int)); + match_origin_as_fallback_ = + static_cast(match_origin_as_fallback_int); + CHECK(iter->ReadBool(&incognito_enabled_)); + + UnpickleHostID(pickle, iter, &host_id_); + + int consumer_instance_type = 0; + CHECK(iter->ReadInt(&consumer_instance_type)); + consumer_instance_type_ = + static_cast(consumer_instance_type); + + UnpickleGlobs(pickle, iter, &globs_); + UnpickleGlobs(pickle, iter, &exclude_globs_); + UnpickleURLPatternSet(pickle, iter, &url_set_); + UnpickleURLPatternSet(pickle, iter, &exclude_url_set_); + UnpickleScripts(pickle, iter, &js_scripts_); + UnpickleScripts(pickle, iter, &css_scripts_); +} + +void UserScript::UnpickleGlobs(const base::Pickle& pickle, + base::PickleIterator* iter, + std::vector* globs) { + uint32_t num_globs = 0; + CHECK(iter->ReadUInt32(&num_globs)); + globs->clear(); + for (uint32_t i = 0; i < num_globs; ++i) { + std::string glob; + CHECK(iter->ReadString(&glob)); + globs->push_back(glob); + } +} + +void UserScript::UnpickleHostID(const base::Pickle& pickle, + base::PickleIterator* iter, + HostID* host_id) { + int type = 0; + std::string id; + CHECK(iter->ReadInt(&type)); + CHECK(iter->ReadString(&id)); + *host_id = HostID(static_cast(type), id); +} + +void UserScript::UnpickleURLPatternSet(const base::Pickle& pickle, + base::PickleIterator* iter, + URLPatternSet* pattern_list) { + uint32_t num_patterns = 0; + CHECK(iter->ReadUInt32(&num_patterns)); + + pattern_list->ClearPatterns(); + for (uint32_t i = 0; i < num_patterns; ++i) { + int valid_schemes; + CHECK(iter->ReadInt(&valid_schemes)); + + std::string pattern_str; + CHECK(iter->ReadString(&pattern_str)); + + URLPattern pattern(kValidUserScriptSchemes); + URLPattern::ParseResult result = pattern.Parse(pattern_str); + CHECK(URLPattern::ParseResult::kSuccess == result) + << URLPattern::GetParseResultString(result) << " " + << pattern_str.c_str(); + + pattern.SetValidSchemes(valid_schemes); + pattern_list->AddPattern(pattern); + } +} + +void UserScript::UnpickleScripts(const base::Pickle& pickle, + base::PickleIterator* iter, + FileList* scripts) { + uint32_t num_files = 0; + CHECK(iter->ReadUInt32(&num_files)); + scripts->clear(); + for (uint32_t i = 0; i < num_files; ++i) { + std::unique_ptr file(new File()); + file->Unpickle(pickle, iter); + scripts->push_back(std::move(file)); + } +} + +UserScriptIDPair::UserScriptIDPair(int id, const HostID& host_id) + : id(id), host_id(host_id) {} + +UserScriptIDPair::UserScriptIDPair(int id) : id(id), host_id(HostID()) {} + +bool operator<(const UserScriptIDPair& a, const UserScriptIDPair& b) { + return a.id < b.id; +} + +} // namespace extensions diff --git a/components/user_scripts/common/user_script.h b/components/user_scripts/common/user_script.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/user_script.h @@ -0,0 +1,403 @@ +// Copyright 2013 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 USERSCRIPTS_COMMON_USER_SCRIPT_H_ +#define USERSCRIPTS_COMMON_USER_SCRIPT_H_ + +#include +#include +#include + +#include "base/files/file_path.h" +#include "base/strings/string_piece.h" +#include "script_constants.h" +#include "host_id.h" +#include "url_pattern.h" +#include "url_pattern_set.h" +#include "url/gurl.h" + +namespace base { +class Pickle; +class PickleIterator; +} + +namespace user_scripts { + +// Represents a user script, either a standalone one, or one that is part of an +// extension. +class UserScript { + public: + // The file extension for standalone user scripts. + static const char kFileExtension[]; + + static int GenerateUserScriptID(); + + // Check if a URL should be treated as a user script and converted to an + // extension. + static bool IsURLUserScript(const GURL& url, const std::string& mime_type); + + // Get the valid user script schemes for the current process. If + // canExecuteScriptEverywhere is true, this will return ALL_SCHEMES. + static int ValidUserScriptSchemes(bool canExecuteScriptEverywhere = false); + + // TODO(rdevlin.cronin) This and RunLocation don't really belong here, since + // they are used for more than UserScripts (e.g., tabs.executeScript()). + // The type of injected script. + enum InjectionType { + // A content script specified in the extension's manifest. + CONTENT_SCRIPT, + // A script injected via, e.g. tabs.executeScript(). + //PROGRAMMATIC_SCRIPT + }; + // The last type of injected script; used for enum verification in IPC. + // Update this if you add more injected script types! + static const InjectionType INJECTION_TYPE_LAST = CONTENT_SCRIPT/*PROGRAMMATIC_SCRIPT*/; + + // Locations that user scripts can be run inside the document. + // The three run locations must strictly follow each other in both load order + // (i.e., start *always* comes before end) and numerically, as we use + // arithmetic checking (e.g., curr == last + 1). So, no bitmasks here!! + enum RunLocation { + UNDEFINED, + DOCUMENT_START, // After the documentElement is created, but before + // anything else happens. + DOCUMENT_END, // After the entire document is parsed. Same as + // DOMContentLoaded. + DOCUMENT_IDLE, // Sometime after DOMContentLoaded, as soon as the document + // is "idle". Currently this uses the simple heuristic of: + // min(DOM_CONTENT_LOADED + TIMEOUT, ONLOAD), but no + // particular injection point is guaranteed. + RUN_DEFERRED, // The user script's injection was deferred for permissions + // reasons, and was executed at a later time. + BROWSER_DRIVEN, // The user script will be injected when triggered by an + // IPC in the browser process. + RUN_LOCATION_LAST // Leave this as the last item. + }; + + // Holds script file info. + class File { + public: + File(const base::FilePath& extension_root, + const base::FilePath& relative_path, + const GURL& url); + File(); + File(const File& other); + ~File(); + + const base::FilePath& extension_root() const { return extension_root_; } + const base::FilePath& relative_path() const { return relative_path_; } + + const GURL& url() const { return url_; } + void set_url(const GURL& url) { url_ = url; } + + // If external_content_ is set returns it as content otherwise it returns + // content_ + const base::StringPiece GetContent() const { + if (external_content_.data()) + return external_content_; + else + return content_; + } + void set_external_content(const base::StringPiece& content) { + external_content_ = content; + } + void set_content(const base::StringPiece& content) { + content_.assign(content.begin(), content.end()); + } + + const std::string& key() const { return key_; } + void set_key(const std::string& key) { + key_ = key; + } + + // Serialization support. The content and FilePath members will not be + // serialized! + void Pickle(base::Pickle* pickle) const; + void Unpickle(const base::Pickle& pickle, base::PickleIterator* iter); + + private: + // Where the script file lives on the disk. We keep the path split so that + // it can be localized at will. + base::FilePath extension_root_; + base::FilePath relative_path_; + + // The url to this script file. + GURL url_; + + // The script content. It can be set to either loaded_content_ or + // externally allocated string. + base::StringPiece external_content_; + + // Set when the content is loaded by LoadContent + std::string content_; + + std::string key_; + }; + + using FileList = std::vector>; + + // Type of a API consumer instance that user scripts will be injected on. + enum ConsumerInstanceType { TAB, WEBVIEW }; + + // Constructor. Default the run location to document end, which is like + // Greasemonkey and probably more useful for typical scripts. + UserScript(); + ~UserScript(); + + // Performs a copy of all fields except file contents. + // static std::unique_ptr CopyMetadataFrom(const UserScript& other); + + 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& version() const { return version_; } + void set_version(const std::string& version) { + version_ = version; + } + + const std::string& key() const { return key_; } + void set_key(const std::string& key) { + key_ = key; + } + + const std::string& file_path() const { return file_path_; } + void set_file_path(const std::string& file_path) { + file_path_ = file_path; + } + + const std::string& url_source() const { return url_source_; } + void set_url_source(const std::string& url_source) { + url_source_ = url_source; + } + + const std::string& description() const { return description_; } + void set_description(const std::string& description) { + description_ = description; + } + + const std::string& parser_error() const { return parser_error_; } + void set_parser_error(const std::string& parser_error) { + parser_error_ = parser_error; + } + + bool force_disabled() const { return force_disabled_; } + void set_force_disabled() { + force_disabled_ = true; + } + + // 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; } + + // Whether to match all frames, or only the top one. + bool match_all_frames() const { return match_all_frames_; } + void set_match_all_frames(bool val) { match_all_frames_ = val; } + + // Whether to match the origin as a fallback if the URL cannot be used + // directly. + MatchOriginAsFallbackBehavior match_origin_as_fallback() const { + return match_origin_as_fallback_; + } + void set_match_origin_as_fallback(MatchOriginAsFallbackBehavior val) { + match_origin_as_fallback_ = 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& globs() const { return globs_; } + void add_glob(const std::string& glob) { globs_.push_back(glob); } + void clear_globs() { globs_.clear(); } + const std::vector& 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. + const URLPatternSet& url_patterns() const { return url_set_; } + void add_url_pattern(const URLPattern& pattern); + const URLPatternSet& exclude_url_patterns() const { + return exclude_url_set_; + } + void add_exclude_url_pattern(const URLPattern& pattern); + + // List of js scripts for this user script + FileList& js_scripts() { return js_scripts_; } + const FileList& js_scripts() const { return js_scripts_; } + + // List of css scripts for this user script + FileList& css_scripts() { return css_scripts_; } + const FileList& css_scripts() const { return css_scripts_; } + + const std::string& extension_id() const { return host_id_.id(); } + + const HostID& host_id() const { return host_id_; } + void set_host_id(const HostID& host_id) { host_id_ = host_id; } + + const ConsumerInstanceType& consumer_instance_type() const { + return consumer_instance_type_; + } + void set_consumer_instance_type( + const ConsumerInstanceType& consumer_instance_type) { + consumer_instance_type_ = consumer_instance_type; + } + + int id() const { return user_script_id_; } + void set_id(int id) { user_script_id_ = id; } + + // TODO(lazyboy): Incognito information is extension specific, it doesn't + // belong here. We should be able to determine this in the renderer/ where it + // is used. + bool is_incognito_enabled() const { return incognito_enabled_; } + void set_incognito_enabled(bool enabled) { incognito_enabled_ = enabled; } + + // Returns true if the script should be applied to the specified URL, false + // otherwise. + bool MatchesURL(const GURL& url) const; + + // Returns true if the script should be applied to the given + // |effective_document_url|. It is the caller's responsibility to calculate + // |effective_document_url| based on match_origin_as_fallback(). + bool MatchesDocument(const GURL& effective_document_url, + bool is_subframe) const; + + // Serializes the UserScript into a pickle. The content of the scripts and + // paths to UserScript::Files will not be serialized! + void Pickle(base::Pickle* pickle) const; + + // Deserializes the script from a pickle. Note that this always succeeds + // because presumably we were the one that pickled it, and we did it + // correctly. + void Unpickle(const base::Pickle& pickle, base::PickleIterator* iter); + + private: + // base::Pickle helper functions used to pickle the individual types of + // components. + void PickleGlobs(base::Pickle* pickle, + const std::vector& globs) const; + void PickleHostID(base::Pickle* pickle, const HostID& host_id) const; + void PickleURLPatternSet(base::Pickle* pickle, + const URLPatternSet& pattern_list) const; + void PickleScripts(base::Pickle* pickle, const FileList& scripts) const; + + // Unpickle helper functions used to unpickle individual types of components. + void UnpickleGlobs(const base::Pickle& pickle, + base::PickleIterator* iter, + std::vector* globs); + void UnpickleHostID(const base::Pickle& pickle, + base::PickleIterator* iter, + HostID* host_id); + void UnpickleURLPatternSet(const base::Pickle& pickle, + base::PickleIterator* iter, + URLPatternSet* pattern_list); + void UnpickleScripts(const base::Pickle& pickle, + base::PickleIterator* iter, + FileList* scripts); + + // The location to run the script inside the document. + RunLocation run_location_ = DOCUMENT_IDLE; + + // 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_; + + // Parser error to show to user + std::string parser_error_; + + // A version number of the script. Only used when parsing Greasemonkey-style + // scripts. + std::string version_; + + // Greasemonkey-style globs that determine pages to inject the script into. + // These are only used with standalone scripts. + std::vector globs_; + std::vector exclude_globs_; + + // URLPatterns that determine pages to inject the script into. These are + // only used with scripts that are part of extensions. + URLPatternSet url_set_; + URLPatternSet exclude_url_set_; + + // List of js scripts defined in content_scripts + FileList js_scripts_; + + // List of css scripts defined in content_scripts + FileList css_scripts_; + + // internal key of scripts + std::string key_; + + std::string file_path_; + + // url source of script + std::string url_source_; + + // The ID of the host this script is a part of. The |ID| of the + // |host_id| can be empty if the script is a "standlone" user script. + HostID host_id_; + + // The type of the consumer instance that the script will be injected. + ConsumerInstanceType consumer_instance_type_ = TAB; + + // The globally-unique id associated with this user script. -1 indicates + // "invalid". + int user_script_id_ = -1; + + // Whether we should try to emulate Greasemonkey's APIs when running this + // script. + bool emulate_greasemonkey_ = false; + + // Whether the user script should run in all frames, or only just the top one. + bool match_all_frames_ = false; + + // Whether the user script should run in frames whose initiator / precursor + // origin matches a match pattern, if an appropriate URL cannot be found for + // the frame for matching purposes, such as in the case of about:, data:, and + // other schemes. + MatchOriginAsFallbackBehavior match_origin_as_fallback_ = + MatchOriginAsFallbackBehavior::kNever; + + // True if the script should be injected into an incognito tab. + bool incognito_enabled_ = false; + + // Script cannot be enabled + bool force_disabled_ = false; + + DISALLOW_COPY_AND_ASSIGN(UserScript); +}; + +// Information we need while removing scripts from a UserScriptLoader. +struct UserScriptIDPair { + UserScriptIDPair(int id, const HostID& host_id); + explicit UserScriptIDPair(int id); + + int id; + HostID host_id; +}; + +bool operator<(const UserScriptIDPair& a, const UserScriptIDPair& b); + +using UserScriptList = std::vector>; + +} // namespace extensions + +#endif // USERSCRIPTS_COMMON_USER_SCRIPT_H_ diff --git a/components/user_scripts/common/user_scripts_features.cc b/components/user_scripts/common/user_scripts_features.cc new file mode 100644 --- /dev/null +++ b/components/user_scripts/common/user_scripts_features.cc @@ -0,0 +1,32 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#include "user_scripts_features.h" + +#include "build/build_config.h" + +namespace user_scripts { + +namespace features { + +const base::Feature kEnableLoggingUserScripts = + {"EnableLoggingUserScripts", + base::FEATURE_DISABLED_BY_DEFAULT}; + +} + +} \ No newline at end of file diff --git a/components/user_scripts/common/user_scripts_features.h b/components/user_scripts/common/user_scripts_features.h new file mode 100644 --- /dev/null +++ b/components/user_scripts/common/user_scripts_features.h @@ -0,0 +1,34 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#ifndef USERSCRIPTS_COMMON_USERSCRIPTS_FEATURES_H_ +#define USERSCRIPTS_COMMON_USERSCRIPTS_FEATURES_H_ + +// This file defines all the base::FeatureList features for the Password Manager +// module. + +#include "base/feature_list.h" + +namespace user_scripts { + +namespace features { + extern const base::Feature kEnableLoggingUserScripts; +} + +} + +#endif \ No newline at end of file diff --git a/components/user_scripts/common/view_type.cc b/components/user_scripts/common/view_type.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/view_type.cc @@ -0,0 +1,39 @@ +// 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. + +#include "view_type.h" + +#include "base/strings/string_piece.h" + +namespace user_scripts { + +bool GetViewTypeFromString(const std::string& view_type, + ViewType* view_type_out) { + // TODO(devlin): This map doesn't contain the following values: + // - VIEW_TYPE_BACKGROUND_CONTENTS + // - VIEW_TYPE_COMPONENT + // - VIEW_TYPE_EXTENSION_GUEST + // Why? Is it just because we don't expose those types to JS? + static const struct { + ViewType type; + base::StringPiece name; + } constexpr kTypeMap[] = { + // {VIEW_TYPE_APP_WINDOW, "APP_WINDOW"}, + // {VIEW_TYPE_EXTENSION_BACKGROUND_PAGE, "BACKGROUND"}, + // {VIEW_TYPE_EXTENSION_DIALOG, "EXTENSION_DIALOG"}, + // {VIEW_TYPE_EXTENSION_POPUP, "POPUP"}, + {VIEW_TYPE_TAB_CONTENTS, "TAB"}, + }; + + for (const auto& entry : kTypeMap) { + if (entry.name == view_type) { + *view_type_out = entry.type; + return true; + } + } + + return false; +} + +} // namespace extensions diff --git a/components/user_scripts/common/view_type.h b/components/user_scripts/common/view_type.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/common/view_type.h @@ -0,0 +1,48 @@ +// 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. + +#ifndef USERSCRIPTS_COMMON_VIEW_TYPE_H_ +#define USERSCRIPTS_COMMON_VIEW_TYPE_H_ + +#include + +namespace user_scripts { + +// Icky RTTI used by a few systems to distinguish the host type of a given +// WebContents. +// +// Do not change or reuse the the entry values in this list as this is used in +// ExtensionViewType enum in tools/metrics/histograms/enums.xml. +// +// TODO(aa): Remove this and teach those systems to keep track of their own +// data. +enum ViewType { + VIEW_TYPE_INVALID = 0, + // VIEW_TYPE_APP_WINDOW = 1, + // VIEW_TYPE_BACKGROUND_CONTENTS = 2, + + // // For custom parts of Chrome if no other type applies. + // VIEW_TYPE_COMPONENT = 3, + + // VIEW_TYPE_EXTENSION_BACKGROUND_PAGE = 4, + // VIEW_TYPE_EXTENSION_DIALOG = 5, + // VIEW_TYPE_EXTENSION_GUEST = 6, + // VIEW_TYPE_EXTENSION_POPUP = 7, + + // Panels were removed in https://crbug.com/571511. + // DEPRECATED_VIEW_TYPE_PANEL = 8, + + VIEW_TYPE_TAB_CONTENTS = 9, + + VIEW_TYPE_LAST = VIEW_TYPE_TAB_CONTENTS +}; + +// Matches the |view_type| to the corresponding ViewType, and populates +// |view_type_out|. Returns true if a match is found. +bool GetViewTypeFromString(const std::string& view_type, + ViewType* view_type_out); + +} // namespace extensions + +#endif // USERSCRIPTS_COMMON_VIEW_TYPE_H_ diff --git a/components/user_scripts/renderer/BUILD.gn b/components/user_scripts/renderer/BUILD.gn new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/BUILD.gn @@ -0,0 +1,67 @@ +# Copyright 2015 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. + +import("//tools/grit/grit_rule.gni") +import("//tools/grit/repack.gni") + +group("user_scripts_resources") { + public_deps = [ + ":user_scripts_renderer_resources", + ] +} + +grit("user_scripts_renderer_resources") { + source = "resources/user_scripts_renderer_resources.grd" + outputs = [ + "grit/user_scripts_renderer_resources.h", + "user_scripts_renderer_resources.pak", + ] + grit_flags = [ + "-E", + "mojom_root=" + rebase_path(root_gen_dir, root_build_dir), + ] +} + +static_library("renderer") { + sources = [ + "extension_frame_helper.cc", + "extension_frame_helper.h", + "injection_host.cc", + "injection_host.h", + "script_injection_manager.cc", + "script_injection_manager.h", + "script_injection_callback.cc", + "script_injection_callback.h", + "script_injection.cc", + "script_injection.h", + "script_injector.h", + "script_context.cc", + "script_context.h", + "scripts_run_info.cc", + "scripts_run_info.h", + "user_script_injector.cc", + "user_script_injector.h", + "user_script_set_manager.cc", + "user_script_set_manager.h", + "user_script_set.cc", + "user_script_set.h", + "user_scripts_dispatcher.cc", + "user_scripts_dispatcher.h", + "user_scripts_renderer_client.cc", + "user_scripts_renderer_client.h", + "web_ui_injection_host.cc", + "web_ui_injection_host.h", + ] + + deps = [ + ":user_scripts_resources", + "//base", + "//content/public/common", + "//content/public/renderer", + "//components/user_scripts/common", + "//mojo/public/cpp/bindings", + "//third_party/blink/public:blink_headers", + "//v8", + ] +} diff --git a/components/user_scripts/renderer/extension_frame_helper.cc b/components/user_scripts/renderer/extension_frame_helper.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/extension_frame_helper.cc @@ -0,0 +1,96 @@ +// Copyright 2013 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 "extension_frame_helper.h" + +#include + +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_util.h" +#include "base/timer/elapsed_timer.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_view.h" +#include "../common/constants.h" +#include "third_party/blink/public/platform/web_security_origin.h" +#include "third_party/blink/public/web/web_console_message.h" +#include "third_party/blink/public/web/web_document.h" +#include "third_party/blink/public/web/web_document_loader.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_settings.h" +#include "third_party/blink/public/web/web_view.h" + +namespace user_scripts { + +namespace { + +base::LazyInstance>::DestructorAtExit + g_frame_helpers = LAZY_INSTANCE_INITIALIZER; + +// Runs every callback in |callbacks_to_be_run_and_cleared| while |frame_helper| +// is valid, and clears |callbacks_to_be_run_and_cleared|. +void RunCallbacksWhileFrameIsValid( + base::WeakPtr frame_helper, + std::vector* callbacks_to_be_run_and_cleared) { + // The JavaScript code can cause re-entrancy. To avoid a deadlock, don't run + // callbacks that are added during the iteration. + std::vector callbacks; + callbacks_to_be_run_and_cleared->swap(callbacks); + for (auto& callback : callbacks) { + std::move(callback).Run(); + if (!frame_helper.get()) + return; // Frame and ExtensionFrameHelper invalidated by callback. + } +} + +} // namespace + +ExtensionFrameHelper::ExtensionFrameHelper(content::RenderFrame* render_frame) + : content::RenderFrameObserver(render_frame), + content::RenderFrameObserverTracker(render_frame), + tab_id_(-1) { + g_frame_helpers.Get().insert(this); +} + +ExtensionFrameHelper::~ExtensionFrameHelper() { + g_frame_helpers.Get().erase(this); +} + +void ExtensionFrameHelper::ScheduleAtDocumentStart( + base::OnceClosure callback) { + document_element_created_callbacks_.push_back(std::move(callback)); +} + +void ExtensionFrameHelper::ScheduleAtDocumentEnd( + base::OnceClosure callback) { + document_load_finished_callbacks_.push_back(std::move(callback)); +} + +void ExtensionFrameHelper::ScheduleAtDocumentIdle( + base::OnceClosure callback) { + document_idle_callbacks_.push_back(std::move(callback)); +} + +void ExtensionFrameHelper::RunScriptsAtDocumentStart() { + RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(), + &document_element_created_callbacks_); + // |this| might be dead by now. +} + +void ExtensionFrameHelper::RunScriptsAtDocumentEnd() { + RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(), + &document_load_finished_callbacks_); + // |this| might be dead by now. +} + +void ExtensionFrameHelper::RunScriptsAtDocumentIdle() { + RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(), + &document_idle_callbacks_); + // |this| might be dead by now. +} + +void ExtensionFrameHelper::OnDestruct() { + delete this; +} + +} // namespace user_scripts diff --git a/components/user_scripts/renderer/extension_frame_helper.h b/components/user_scripts/renderer/extension_frame_helper.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/extension_frame_helper.h @@ -0,0 +1,92 @@ +// Copyright 2013 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 USERSCRIPTS_RENDERER_EXTENSION_FRAME_HELPER_H_ +#define USERSCRIPTS_RENDERER_EXTENSION_FRAME_HELPER_H_ + +#include +#include + +#include "base/callback_forward.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "content/public/renderer/render_frame_observer.h" +#include "content/public/renderer/render_frame_observer_tracker.h" +#include "../common/view_type.h" +#include "third_party/blink/public/mojom/devtools/console_message.mojom.h" +#include "v8/include/v8.h" + +struct ExtensionMsg_ExternalConnectionInfo; +struct ExtensionMsg_TabConnectionInfo; + +namespace base { +class ListValue; +} + +namespace user_scripts { + +class Dispatcher; +struct Message; +struct PortId; +class ScriptContext; + +// RenderFrame-level plumbing for extension features. +class ExtensionFrameHelper + : public content::RenderFrameObserver, + public content::RenderFrameObserverTracker { + public: + ExtensionFrameHelper(content::RenderFrame* render_frame /*, + Dispatcher* extension_dispatcher*/); + ~ExtensionFrameHelper() override; + + int tab_id() const { return tab_id_; } + + // Called when the document element has been inserted in this frame. This + // method may invoke untrusted JavaScript code that invalidate the frame and + // this ExtensionFrameHelper. + void RunScriptsAtDocumentStart(); + + // Called after the DOMContentLoaded event has fired. + void RunScriptsAtDocumentEnd(); + + // Called before the window.onload event is fired. + void RunScriptsAtDocumentIdle(); + + // Schedule a callback, to be run at the next RunScriptsAtDocumentStart + // notification. Only call this when you are certain that there will be such a + // notification, e.g. from RenderFrameObserver::DidCreateDocumentElement. + // Otherwise the callback is never invoked, or invoked for a document that you + // were not expecting. + void ScheduleAtDocumentStart(base::OnceClosure callback); + + // Schedule a callback, to be run at the next RunScriptsAtDocumentEnd call. + void ScheduleAtDocumentEnd(base::OnceClosure callback); + + // Schedule a callback, to be run at the next RunScriptsAtDocumentIdle call. + void ScheduleAtDocumentIdle(base::OnceClosure callback); + + private: + + void OnDestruct() override; + + // The id of the tab the render frame is attached to. + int tab_id_; + + // Callbacks to be run at the next RunScriptsAtDocumentStart notification. + std::vector document_element_created_callbacks_; + + // Callbacks to be run at the next RunScriptsAtDocumentEnd notification. + std::vector document_load_finished_callbacks_; + + // Callbacks to be run at the next RunScriptsAtDocumentIdle notification. + std::vector document_idle_callbacks_; + + base::WeakPtrFactory weak_ptr_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(ExtensionFrameHelper); +}; + +} // namespace extensions + +#endif // USERSCRIPTS_RENDERER_EXTENSION_FRAME_HELPER_H_ diff --git a/components/user_scripts/renderer/injection_host.cc b/components/user_scripts/renderer/injection_host.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/injection_host.cc @@ -0,0 +1,12 @@ +// Copyright 2015 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 "injection_host.h" + +InjectionHost::InjectionHost(const HostID& host_id) : + id_(host_id) { +} + +InjectionHost::~InjectionHost() { +} diff --git a/components/user_scripts/renderer/injection_host.h b/components/user_scripts/renderer/injection_host.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/injection_host.h @@ -0,0 +1,42 @@ +// Copyright 2015 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 USERSCRIPTS_RENDERER_INJECTION_HOST_H_ +#define USERSCRIPTS_RENDERER_INJECTION_HOST_H_ + +#include "base/macros.h" +#include "../common/host_id.h" +#include "url/gurl.h" + +namespace content { +class RenderFrame; +} + +// An interface for all kinds of hosts who own user scripts. +class InjectionHost { + public: + InjectionHost(const HostID& host_id); + virtual ~InjectionHost(); + + // Returns the CSP to be used for the isolated world. Currently this only + // bypasses the main world CSP. If null is returned, the main world CSP is not + // bypassed. + virtual const std::string* GetContentSecurityPolicy() const = 0; + + // The base url for the host. + virtual const GURL& url() const = 0; + + // The human-readable name of the host. + virtual const std::string& name() const = 0; + + const HostID& id() const { return id_; } + + private: + // The ID of the host. + HostID id_; + + DISALLOW_COPY_AND_ASSIGN(InjectionHost); +}; + +#endif // USERSCRIPTS_RENDERER_INJECTION_HOST_H_ diff --git a/components/user_scripts/renderer/resources/greasemonkey_api.js b/components/user_scripts/renderer/resources/greasemonkey_api.js new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/resources/greasemonkey_api.js @@ -0,0 +1,82 @@ +// Copyright 2014 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. + +// ----------------------------------------------------------------------------- +// NOTE: If you change this file you need to touch renderer_resources.grd to +// have your change take effect. +// ----------------------------------------------------------------------------- + +// Partial implementation of the Greasemonkey API, see: +// http://wiki.greasespot.net/Greasemonkey_Manual:APIs + +function GM_addStyle(css) { + var parent = document.getElementsByTagName("head")[0]; + if (!parent) { + parent = document.documentElement; + } + var style = document.createElement("style"); + style.type = "text/css"; + var textNode = document.createTextNode(css); + style.appendChild(textNode); + parent.appendChild(style); +} + +function GM_xmlhttpRequest(details) { + function setupEvent(xhr, url, eventName, callback) { + xhr[eventName] = function () { + var isComplete = xhr.readyState == 4; + var responseState = { + responseText: xhr.responseText, + readyState: xhr.readyState, + responseHeaders: isComplete ? xhr.getAllResponseHeaders() : "", + status: isComplete ? xhr.status : 0, + statusText: isComplete ? xhr.statusText : "", + finalUrl: isComplete ? url : "" + }; + callback(responseState); + }; + } + + var xhr = new XMLHttpRequest(); + var eventNames = ["onload", "onerror", "onreadystatechange"]; + for (var i = 0; i < eventNames.length; i++ ) { + var eventName = eventNames[i]; + if (eventName in details) { + setupEvent(xhr, details.url, eventName, details[eventName]); + } + } + + xhr.open(details.method, details.url); + + if (details.overrideMimeType) { + xhr.overrideMimeType(details.overrideMimeType); + } + if (details.headers) { + for (var header in details.headers) { + xhr.setRequestHeader(header, details.headers[header]); + } + } + xhr.send(details.data ? details.data : null); +} + +function GM_openInTab(url) { + window.open(url, ""); +} + +function GM_log(message) { + window.console.log(message); +} + +(function() { + function generateGreasemonkeyStub(name) { + return function() { + console.log("%s is not supported.", name); + }; + } + + var apis = ["GM_getValue", "GM_setValue", "GM_registerMenuCommand"]; + for (var i = 0, api; api = apis[i]; i++) { + window[api] = generateGreasemonkeyStub(api); + } +})(); diff --git a/components/user_scripts/renderer/resources/user_scripts_renderer_resources.grd b/components/user_scripts/renderer/resources/user_scripts_renderer_resources.grd new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/resources/user_scripts_renderer_resources.grd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/components/user_scripts/renderer/script_context.cc b/components/user_scripts/renderer/script_context.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/script_context.cc @@ -0,0 +1,215 @@ +// Copyright 2014 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 "script_context.h" + +#include "base/command_line.h" +#include "base/containers/flat_set.h" +#include "base/containers/contains.h" +#include "base/logging.h" +#include "base/no_destructor.h" +#include "base/stl_util.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/values.h" +#include "content/public/common/content_switches.h" +#include "content/public/common/url_constants.h" +#include "content/public/renderer/render_frame.h" +#include "../common/constants.h" +#include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h" +#include "third_party/blink/public/platform/web_security_origin.h" +#include "third_party/blink/public/web/web_document.h" +#include "third_party/blink/public/web/web_document_loader.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_navigation_params.h" +#include "v8/include/v8.h" + +namespace user_scripts { + +namespace { + +GURL GetEffectiveDocumentURL( + blink::WebLocalFrame* frame, + const GURL& document_url, + MatchOriginAsFallbackBehavior match_origin_as_fallback, + bool allow_inaccessible_parents) { + auto should_consider_origin = [document_url, match_origin_as_fallback]() { + switch (match_origin_as_fallback) { + case MatchOriginAsFallbackBehavior::kNever: + return false; + case MatchOriginAsFallbackBehavior::kMatchForAboutSchemeAndClimbTree: + return document_url.SchemeIs(url::kAboutScheme); + case MatchOriginAsFallbackBehavior::kAlways: + // TODO(devlin): Add more schemes here - blob, filesystem, etc. + return document_url.SchemeIs(url::kAboutScheme) || + document_url.SchemeIs(url::kDataScheme); + } + + NOTREACHED(); + }; + + // If we don't need to consider the origin, we're done. + if (!should_consider_origin()) + return document_url; + + // Get the "security origin" for the frame. For about: frames, this is the + // origin of that of the controlling frame - e.g., an about:blank frame on + // https://example.com will have the security origin of https://example.com. + // Other frames, like data: frames, will have an opaque origin. For these, + // we can get the precursor origin. + const blink::WebSecurityOrigin web_frame_origin = frame->GetSecurityOrigin(); + const url::Origin frame_origin = web_frame_origin; + const url::SchemeHostPort& tuple_or_precursor_tuple = + frame_origin.GetTupleOrPrecursorTupleIfOpaque(); + + // When there's no valid tuple (which can happen in the case of e.g. a + // browser-initiated navigation to an opaque URL), there's no origin to + // fallback to. Bail. + if (!tuple_or_precursor_tuple.IsValid()) + return document_url; + + const url::Origin origin_or_precursor_origin = + url::Origin::Create(tuple_or_precursor_tuple.GetURL()); + + if (!allow_inaccessible_parents && + !web_frame_origin.CanAccess( + blink::WebSecurityOrigin(origin_or_precursor_origin))) { + // The frame can't access its precursor. Bail. + return document_url; + } + + // Looks like the initiator origin is an appropriate fallback! + + if (match_origin_as_fallback == MatchOriginAsFallbackBehavior::kAlways) { + // The easy case! We use the origin directly. We're done. + return origin_or_precursor_origin.GetURL(); + } + + DCHECK_EQ(MatchOriginAsFallbackBehavior::kMatchForAboutSchemeAndClimbTree, + match_origin_as_fallback); + + // Unfortunately, in this case, we have to climb the frame tree. This is for + // match patterns that are associated with paths as well, not just origins. + // For instance, if an extension wants to run on google.com/maps/* with + // match_about_blank true, then it should run on about:-scheme frames created + // by google.com/maps, but not about:-scheme frames created by google.com + // (which is what the precursor tuple origin would be). + + // Traverse the frame/window hierarchy to find the closest non-about:-page + // with the same origin as the precursor and return its URL. + // Note: This can return the incorrect result, e.g. if a parent frame + // navigates a grandchild frame. + blink::WebFrame* parent = frame; + GURL parent_url; + blink::WebDocument parent_document; + base::flat_set already_visited_frames; + do { + already_visited_frames.insert(parent); + if (parent->Parent()) + parent = parent->Parent(); + else + parent = parent->Opener(); + + // Avoid an infinite loop - see https://crbug.com/568432 and + // https://crbug.com/883526. + if (base::Contains(already_visited_frames, parent)) + return document_url; + + parent_document = parent && parent->IsWebLocalFrame() + ? parent->ToWebLocalFrame()->GetDocument() + : blink::WebDocument(); + + // We reached the end of the ancestral chain without finding a valid parent, + // or found a remote web frame (in which case, it's a different origin). + // Bail and use the original URL. + if (parent_document.IsNull()) + return document_url; + + url::SchemeHostPort parent_tuple_or_precursor_tuple = + url::Origin(parent->GetSecurityOrigin()) + .GetTupleOrPrecursorTupleIfOpaque(); + if (!parent_tuple_or_precursor_tuple.IsValid() || + parent_tuple_or_precursor_tuple != tuple_or_precursor_tuple) { + // The parent has a different tuple origin than frame; this could happen + // in edge cases where a parent navigates an iframe or popup of a child + // frame at a different origin. [1] In this case, bail, since we can't + // find a full URL (i.e., one including the path) with the same security + // origin to use for the frame in question. + // [1] Consider a frame tree like: + // + // + // + // + // Frame "a" is cross-origin from the top-level frame, and so the + // example.com top-level frame can't directly access frame "b". However, + // it can navigate it through + // window.frames[0].frames[0].location.href = 'about:blank'; + // In that case, the precursor origin tuple origin of frame "b" would be + // example.com, but the parent tuple origin is a.com. + // Note that usually, this would have bailed earlier with a remote frame, + // but it may not if we're at the process limit. + return document_url; + } + + parent_url = GURL(parent_document.Url()); + } while (parent_url.SchemeIs(url::kAboutScheme)); + + DCHECK(!parent_url.is_empty()); + DCHECK(!parent_document.IsNull()); + + // We should know that the frame can access the parent document (unless we + // explicitly allow it not to), since it has the same tuple origin as the + // frame, and we checked the frame access above. + DCHECK(allow_inaccessible_parents || + web_frame_origin.CanAccess(parent_document.GetSecurityOrigin())); + return parent_url; +} + +using FrameToDocumentLoader = + base::flat_map; + +FrameToDocumentLoader& FrameDocumentLoaderMap() { + static base::NoDestructor map; + return *map; +} + +blink::WebDocumentLoader* CurrentDocumentLoader( + const blink::WebLocalFrame* frame) { + auto& map = FrameDocumentLoaderMap(); + auto it = map.find(frame); + return it == map.end() ? frame->GetDocumentLoader() : it->second; +} + +} // namespace + +// static +GURL ScriptContext::GetDocumentLoaderURLForFrame( + const blink::WebLocalFrame* frame) { + // Normally we would use frame->document().url() to determine the document's + // URL, but to decide whether to inject a content script, we use the URL from + // the data source. This "quirk" helps prevents content scripts from + // inadvertently adding DOM elements to the compose iframe in Gmail because + // the compose iframe's dataSource URL is about:blank, but the document URL + // changes to match the parent document after Gmail document.writes into + // it to create the editor. + // http://code.google.com/p/chromium/issues/detail?id=86742 + blink::WebDocumentLoader* document_loader = CurrentDocumentLoader(frame); + return document_loader ? GURL(document_loader->GetUrl()) : GURL(); +} + +// static +GURL ScriptContext::GetEffectiveDocumentURLForInjection( + blink::WebLocalFrame* frame, + const GURL& document_url, + MatchOriginAsFallbackBehavior match_origin_as_fallback) { + // We explicitly allow inaccessible parents here. Extensions should still be + // able to inject into a sandboxed iframe if it has access to the embedding + // origin. + constexpr bool allow_inaccessible_parents = true; + return GetEffectiveDocumentURL(frame, document_url, match_origin_as_fallback, + allow_inaccessible_parents); +} + +} // namespace extensions diff --git a/components/user_scripts/renderer/script_context.h b/components/user_scripts/renderer/script_context.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/script_context.h @@ -0,0 +1,70 @@ +// Copyright 2014 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 USERSCRIPTS_RENDERER_SCRIPT_CONTEXT_H_ +#define USERSCRIPTS_RENDERER_SCRIPT_CONTEXT_H_ + +#include +#include +#include +#include + +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "base/threading/thread_checker.h" +#include "base/unguessable_token.h" +#include "../common/script_constants.h" +#include "script_injection_callback.h" +#include "url/gurl.h" +#include "v8/include/v8.h" + +namespace blink { +class WebDocumentLoader; +class WebLocalFrame; +} + +namespace content { +class RenderFrame; +} + +namespace user_scripts { + +// Extensions wrapper for a v8::Context. +// +// v8::Contexts can be constructed on any thread, and must only be accessed or +// destroyed that thread. +// +// Note that ScriptContexts bound to worker threads will not have the full +// functionality as those bound to the main RenderThread. +class ScriptContext { + public: + // TODO(devlin): Move all these Get*URL*() methods out of here? While they are + // vaguely ScriptContext related, there's enough here that they probably + // warrant another class or utility file. + + // Utility to get the URL we will match against for a frame. If the frame has + // committed, this is the commited URL. Otherwise it is the provisional URL. + // The returned URL may be invalid. + static GURL GetDocumentLoaderURLForFrame(const blink::WebLocalFrame* frame); + + // Used to determine the "effective" URL for extension script injection. + // If |document_url| is an about: or data: URL, returns the URL of the first + // frame without an about: or data: URL that matches the initiator origin. + // This may not be the immediate parent. Returns |document_url| if it is not + // an about: or data: URL, if |match_origin_as_fallback| is set to not match, + // or if a suitable parent cannot be found. + // Considers parent contexts that cannot be accessed (as is the case for + // sandboxed frames). + static GURL GetEffectiveDocumentURLForInjection( + blink::WebLocalFrame* frame, + const GURL& document_url, + MatchOriginAsFallbackBehavior match_origin_as_fallback); + +// DISALLOW_COPY_AND_ASSIGN(ScriptContext); +}; + +} // namespace extensions + +#endif // USERSCRIPTS_RENDERER_SCRIPT_CONTEXT_H_ diff --git a/components/user_scripts/renderer/script_injection.cc b/components/user_scripts/renderer/script_injection.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/script_injection.cc @@ -0,0 +1,343 @@ +// Copyright 2014 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 "script_injection.h" + +#include +#include + +#include "base/bind.h" +#include "base/feature_list.h" +#include "base/lazy_instance.h" +#include "base/macros.h" +#include "base/metrics/histogram_macros.h" +#include "base/timer/elapsed_timer.h" +#include "base/values.h" +#include "base/logging.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_frame_observer.h" +#include "content/public/renderer/v8_value_converter.h" +#include "../common/host_id.h" +#include "script_injection_callback.h" +#include "scripts_run_info.h" +#include "third_party/blink/public/platform/web_isolated_world_info.h" +#include "third_party/blink/public/platform/web_security_origin.h" +#include "third_party/blink/public/platform/web_string.h" +#include "third_party/blink/public/web/web_document.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_script_source.h" +#include "url/gurl.h" + +namespace user_scripts { + +namespace { + +using IsolatedWorldMap = std::map; +base::LazyInstance::DestructorAtExit g_isolated_worlds = + LAZY_INSTANCE_INITIALIZER; + +const int64_t kInvalidRequestId = -1; + +// Gets the isolated world ID to use for the given |injection_host|. If no +// isolated world has been created for that |injection_host| one will be created +// and initialized. +int GetIsolatedWorldIdForInstance(const InjectionHost* injection_host) { + static int g_next_isolated_world_id = 1; // Embedder isolated worlds can use IDs in [1, 1<<29). + + IsolatedWorldMap& isolated_worlds = g_isolated_worlds.Get(); + + int id = 0; + const std::string& key = injection_host->id().id(); + auto iter = isolated_worlds.find(key); + if (iter != isolated_worlds.end()) { + id = iter->second; + } else { + id = g_next_isolated_world_id++; + // This map will tend to pile up over time, but realistically, you're never + // going to have enough injection hosts for it to matter. + isolated_worlds[key] = id; + } + + blink::WebIsolatedWorldInfo info; + info.security_origin = + blink::WebSecurityOrigin::Create(injection_host->url()); + info.human_readable_name = blink::WebString::FromUTF8(injection_host->name()); + info.stable_id = blink::WebString::FromUTF8(key); + + const std::string* csp = injection_host->GetContentSecurityPolicy(); + if (csp) + info.content_security_policy = blink::WebString::FromUTF8(*csp); + + // Even though there may be an existing world for this |injection_host|'s key, + // the properties may have changed (e.g. due to an extension update). + // Overwrite any existing entries. + blink::SetIsolatedWorldInfo(id, info); + + return id; +} + +// This class manages its own lifetime. +class TimedScriptInjectionCallback : public ScriptInjectionCallback { + public: + TimedScriptInjectionCallback(base::WeakPtr injection) + : ScriptInjectionCallback( + base::BindOnce(&TimedScriptInjectionCallback::OnCompleted, + base::Unretained(this))), + injection_(injection) {} + ~TimedScriptInjectionCallback() override {} + + void OnCompleted(const std::vector>& result) { + if (injection_) { + base::TimeTicks timestamp(base::TimeTicks::Now()); + absl::optional elapsed; + // If the script will never execute (such as if the context is destroyed), + // willExecute() will not be called, but OnCompleted() will. Only log a + // time for execution if the script, in fact, executed. + if (!start_time_.is_null()) + elapsed = timestamp - start_time_; + injection_->OnJsInjectionCompleted(result, elapsed); + } + } + + void WillExecute() override { + start_time_ = base::TimeTicks::Now(); + } + + private: + base::WeakPtr injection_; + base::TimeTicks start_time_; +}; + +} // namespace + +// Watches for the deletion of a RenderFrame, after which is_valid will return +// false. +class ScriptInjection::FrameWatcher : public content::RenderFrameObserver { + public: + FrameWatcher(content::RenderFrame* render_frame, + ScriptInjection* injection) + : content::RenderFrameObserver(render_frame), + injection_(injection) {} + ~FrameWatcher() override {} + + private: + void WillDetach() override { injection_->invalidate_render_frame(); } + void OnDestruct() override { injection_->invalidate_render_frame(); } + + ScriptInjection* injection_; + + DISALLOW_COPY_AND_ASSIGN(FrameWatcher); +}; + +// static +std::string ScriptInjection::GetHostIdForIsolatedWorld(int isolated_world_id) { + const IsolatedWorldMap& isolated_worlds = g_isolated_worlds.Get(); + + for (const auto& iter : isolated_worlds) { + if (iter.second == isolated_world_id) + return iter.first; + } + return std::string(); +} + +// static +void ScriptInjection::RemoveIsolatedWorld(const std::string& host_id) { + g_isolated_worlds.Get().erase(host_id); +} + +ScriptInjection::ScriptInjection( + std::unique_ptr injector, + content::RenderFrame* render_frame, + std::unique_ptr injection_host, + UserScript::RunLocation run_location, + bool log_activity) + : injector_(std::move(injector)), + render_frame_(render_frame), + injection_host_(std::move(injection_host)), + run_location_(run_location), + request_id_(kInvalidRequestId), + complete_(false), + did_inject_js_(false), + log_activity_(log_activity), + frame_watcher_(new FrameWatcher(render_frame, this)) { + CHECK(injection_host_.get()); +} + +ScriptInjection::~ScriptInjection() { + if (!complete_) + NotifyWillNotInject(ScriptInjector::WONT_INJECT); +} + +ScriptInjection::InjectionResult ScriptInjection::TryToInject( + UserScript::RunLocation current_location, + ScriptsRunInfo* scripts_run_info, + CompletionCallback async_completion_callback) { + if (current_location < run_location_) + return INJECTION_WAITING; // Wait for the right location. + + if (request_id_ != kInvalidRequestId) { + // We're waiting for permission right now, try again later. + return INJECTION_WAITING; + } + + if (!injection_host_) { + NotifyWillNotInject(ScriptInjector::EXTENSION_REMOVED); + return INJECTION_FINISHED; // We're done. + } + + InjectionResult result = Inject(scripts_run_info); + // If the injection is blocked, we need to set the manager so we can + // notify it upon completion. + if (result == INJECTION_BLOCKED) + async_completion_callback_ = std::move(async_completion_callback); + return result; +} + +ScriptInjection::InjectionResult ScriptInjection::OnPermissionGranted( + ScriptsRunInfo* scripts_run_info) { + if (!injection_host_) { + NotifyWillNotInject(ScriptInjector::EXTENSION_REMOVED); + return INJECTION_FINISHED; + } + + return Inject(scripts_run_info); +} + +void ScriptInjection::OnHostRemoved() { + injection_host_.reset(nullptr); +} + +void ScriptInjection::NotifyWillNotInject( + ScriptInjector::InjectFailureReason reason) { + complete_ = true; + injector_->OnWillNotInject(reason, render_frame_); +} + +ScriptInjection::InjectionResult ScriptInjection::Inject( + ScriptsRunInfo* scripts_run_info) { + DCHECK(injection_host_); + //DCHECK(scripts_run_info); + DCHECK(!complete_); + bool should_inject_js = injector_->ShouldInjectJs( + run_location_, scripts_run_info->executing_scripts[host_id().id()]); + bool should_inject_css = injector_->ShouldInjectCss( + run_location_, scripts_run_info->injected_stylesheets[host_id().id()]); + + // This can happen if the extension specified a script to + // be run in multiple rules, and the script has already run. + // See crbug.com/631247. + if (!should_inject_js && !should_inject_css) { + return INJECTION_FINISHED; + } + + if (should_inject_js) + InjectJs(&(scripts_run_info->executing_scripts[host_id().id()]), + &(scripts_run_info->num_js)); + if (should_inject_css) + InjectCss(&(scripts_run_info->injected_stylesheets[host_id().id()]), + &(scripts_run_info->num_css)); + + complete_ = did_inject_js_ || !should_inject_js; + + if (complete_) { + injector_->OnInjectionComplete(std::move(execution_result_), run_location_, + render_frame_); + } else { + ++scripts_run_info->num_blocking_js; + } + + return complete_ ? INJECTION_FINISHED : INJECTION_BLOCKED; +} + +void ScriptInjection::InjectJs(std::set* executing_scripts, + size_t* num_injected_js_scripts) { + DCHECK(!did_inject_js_); + std::vector sources = injector_->GetJsSources( + run_location_, executing_scripts, num_injected_js_scripts); + DCHECK(!sources.empty()); + int world_id = GetIsolatedWorldIdForInstance(injection_host_.get()); + bool is_user_gesture = injector_->IsUserGesture(); + + std::unique_ptr callback( + new TimedScriptInjectionCallback(weak_ptr_factory_.GetWeakPtr())); + + base::ElapsedTimer exec_timer; + + // For content scripts executing during page load, we run them asynchronously + // in order to reduce UI jank experienced by the user. (We don't do this for + // DOCUMENT_START scripts, because there's no UI to jank until after those + // run, so we run them as soon as we can.) + // Note: We could potentially also run deferred and browser-driven scripts + // asynchronously; however, these are rare enough that there probably isn't + // UI jank. If this changes, we can update this. + bool should_execute_asynchronously = + injector_->script_type() == UserScript::CONTENT_SCRIPT && + (run_location_ == UserScript::DOCUMENT_END || + run_location_ == UserScript::DOCUMENT_IDLE); + blink::WebLocalFrame::ScriptExecutionType execution_option = + should_execute_asynchronously + ? blink::WebLocalFrame::kAsynchronousBlockingOnload + : blink::WebLocalFrame::kSynchronous; + + render_frame_->GetWebFrame()->RequestExecuteScriptInIsolatedWorld( + world_id, &sources.front(), sources.size(), is_user_gesture, + execution_option, callback.release(), + blink::BackForwardCacheAware::kPossiblyDisallow); +} + +void ScriptInjection::OnJsInjectionCompleted( + const std::vector>& results, + absl::optional elapsed) { + DCHECK(!did_inject_js_); + + bool expects_results = injector_->ExpectsResults(); + if (expects_results) { + if (!results.empty() && !results[0].IsEmpty()) { + // Right now, we only support returning single results (per frame). + // It's safe to always use the main world context when converting + // here. V8ValueConverterImpl shouldn't actually care about the + // context scope, and it switches to v8::Object's creation context + // when encountered. + v8::Local context = + render_frame_->GetWebFrame()->MainWorldScriptContext(); + execution_result_ = + content::V8ValueConverter::Create()->FromV8Value(results[0], context); + } + if (!execution_result_.get()) + execution_result_ = std::make_unique(); + } + did_inject_js_ = true; + + // If |async_completion_callback_| is set, it means the script finished + // asynchronously, and we should run it. + if (!async_completion_callback_.is_null()) { + complete_ = true; + injector_->OnInjectionComplete(std::move(execution_result_), run_location_, + render_frame_); + // Warning: this object can be destroyed after this line! + std::move(async_completion_callback_).Run(this); + } +} + +void ScriptInjection::InjectCss(std::set* injected_stylesheets, + size_t* num_injected_stylesheets) { + std::vector css_sources = injector_->GetCssSources( + run_location_, injected_stylesheets, num_injected_stylesheets); + blink::WebLocalFrame* web_frame = render_frame_->GetWebFrame(); + // Default CSS origin is "author", but can be overridden to "user" by scripts. + absl::optional css_origin = injector_->GetCssOrigin(); + blink::WebDocument::CSSOrigin blink_css_origin = + css_origin && *css_origin == CSS_ORIGIN_USER + ? blink::WebDocument::kUserOrigin + : blink::WebDocument::kAuthorOrigin; + blink::WebStyleSheetKey style_sheet_key; + if (const absl::optional& injection_key = + injector_->GetInjectionKey()) + style_sheet_key = blink::WebString::FromASCII(*injection_key); + for (const blink::WebString& css : css_sources) + web_frame->GetDocument().InsertStyleSheet(css, &style_sheet_key, + blink_css_origin); +} + +} // namespace extensions diff --git a/components/user_scripts/renderer/script_injection.h b/components/user_scripts/renderer/script_injection.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/script_injection.h @@ -0,0 +1,155 @@ +// Copyright 2014 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 USERSCRIPTS_RENDERER_SCRIPT_INJECTION_H_ +#define USERSCRIPTS_RENDERER_SCRIPT_INJECTION_H_ + +#include + +#include +#include + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "../common/user_script.h" +#include "injection_host.h" +#include "script_injector.h" + +struct HostID; + +namespace content { +class RenderFrame; +} + +namespace v8 { +class Value; +template class Local; +} + +namespace user_scripts { +struct ScriptsRunInfo; + +// A script wrapper which is aware of whether or not it is allowed to execute, +// and contains the implementation to do so. +class ScriptInjection { + public: + enum InjectionResult { + INJECTION_FINISHED, + INJECTION_BLOCKED, + INJECTION_WAITING + }; + + using CompletionCallback = base::OnceCallback; + + // Return the id of the injection host associated with the given world. + static std::string GetHostIdForIsolatedWorld(int world_id); + + // Remove the isolated world associated with the given injection host. + static void RemoveIsolatedWorld(const std::string& host_id); + + ScriptInjection(std::unique_ptr injector, + content::RenderFrame* render_frame, + std::unique_ptr injection_host, + UserScript::RunLocation run_location, + bool log_activity); + ~ScriptInjection(); + + // Try to inject the script at the |current_location|. This returns + // INJECTION_FINISHED if injection has injected or will never inject, returns + // INJECTION_BLOCKED if injection is running asynchronously and has not + // finished yet, returns INJECTION_WAITING if injections is delayed (either + // for permission purposes or because |current_location| is not the designated + // |run_location_|). + // If INJECTION_BLOCKED is returned, |async_completion_callback| will be + // called upon completion. + InjectionResult TryToInject( + UserScript::RunLocation current_location, + ScriptsRunInfo* scripts_run_info, + CompletionCallback async_completion_callback); + + // Called when permission for the given injection has been granted. + // Returns INJECTION_FINISHED if injection has injected or will never inject, + // returns INJECTION_BLOCKED if injection is ran asynchronously. + InjectionResult OnPermissionGranted(ScriptsRunInfo* scripts_run_info); + + // Resets the pointer of the injection host when the host is gone. + void OnHostRemoved(); + + void invalidate_render_frame() { render_frame_ = nullptr; } + + // Accessors. + content::RenderFrame* render_frame() const { return render_frame_; } + const HostID& host_id() const { return injection_host_->id(); } + int64_t request_id() const { return request_id_; } + + // Called when JS injection for the given frame has been completed or + // cancelled. + void OnJsInjectionCompleted(const std::vector>& results, + absl::optional elapsed); + + private: + class FrameWatcher; + + // Sends a message to the browser to request permission to inject. + void RequestPermissionFromBrowser(); + + // Injects the script. Returns INJECTION_FINISHED if injection has finished, + // otherwise INJECTION_BLOCKED. + InjectionResult Inject(ScriptsRunInfo* scripts_run_info); + + // Inject any JS scripts into the frame for the injection. + void InjectJs(std::set* executing_scripts, + size_t* num_injected_js_scripts); + + // Inject any CSS source into the frame for the injection. + void InjectCss(std::set* injected_stylesheets, + size_t* num_injected_stylesheets); + + // Notify that we will not inject, and mark it as acknowledged. + void NotifyWillNotInject(ScriptInjector::InjectFailureReason reason); + + // The injector for this injection. + std::unique_ptr injector_; + + // The RenderFrame into which this should inject the script. + content::RenderFrame* render_frame_; + + // The associated injection host. + std::unique_ptr injection_host_; + + // The location in the document load at which we inject the script. + UserScript::RunLocation run_location_; + + // This injection's request id. This will be -1 unless the injection is + // currently waiting on permission. + int64_t request_id_; + + // Whether or not the injection is complete, either via injecting the script + // or because it will never complete. + bool complete_; + + // Whether or not the injection successfully injected JS. + bool did_inject_js_; + + // Whether or not we should log dom activity for this injection. + bool log_activity_; + + // Results storage. + std::unique_ptr execution_result_; + + // The callback to run upon completing asynchronously. + CompletionCallback async_completion_callback_; + + // A helper class to hold the render frame and watch for its deletion. + std::unique_ptr frame_watcher_; + + base::WeakPtrFactory weak_ptr_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(ScriptInjection); +}; + +} // namespace extensions + +#endif // USERSCRIPTS_RENDERER_SCRIPT_INJECTION_H_ diff --git a/components/user_scripts/renderer/script_injection_callback.cc b/components/user_scripts/renderer/script_injection_callback.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/script_injection_callback.cc @@ -0,0 +1,25 @@ +// Copyright 2015 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 "script_injection_callback.h" + +#include "third_party/blink/public/platform/web_vector.h" + +namespace user_scripts { + +ScriptInjectionCallback::ScriptInjectionCallback( + CompleteCallback injection_completed_callback) + : injection_completed_callback_(std::move(injection_completed_callback)) {} + +ScriptInjectionCallback::~ScriptInjectionCallback() { +} + +void ScriptInjectionCallback::Completed( + const blink::WebVector>& result) { + std::vector> stl_result(result.begin(), result.end()); + std::move(injection_completed_callback_).Run(stl_result); + delete this; +} + +} // namespace extensions diff --git a/components/user_scripts/renderer/script_injection_callback.h b/components/user_scripts/renderer/script_injection_callback.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/script_injection_callback.h @@ -0,0 +1,39 @@ +// Copyright 2015 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 USERSCRIPTS_RENDERER_SCRIPT_INJECTION_CALLBACK_H_ +#define USERSCRIPTS_RENDERER_SCRIPT_INJECTION_CALLBACK_H_ + +#include + +#include "base/callback.h" +#include "base/macros.h" +#include "third_party/blink/public/web/web_script_execution_callback.h" +#include "v8/include/v8.h" + +namespace user_scripts { + +// A wrapper around a callback to notify a script injection when injection +// completes. +// This class manages its own lifetime. +class ScriptInjectionCallback : public blink::WebScriptExecutionCallback { + public: + using CompleteCallback = + base::OnceCallback>& result)>; + + explicit ScriptInjectionCallback( + CompleteCallback injection_completed_callback); + ~ScriptInjectionCallback() override; + + void Completed(const blink::WebVector>& result) override; + + private: + CompleteCallback injection_completed_callback_; + + DISALLOW_COPY_AND_ASSIGN(ScriptInjectionCallback); +}; + +} // namespace extensions + +#endif // USERSCRIPTS_RENDERER_SCRIPT_INJECTION_CALLBACK_H_ diff --git a/components/user_scripts/renderer/script_injection_manager.cc b/components/user_scripts/renderer/script_injection_manager.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/script_injection_manager.cc @@ -0,0 +1,417 @@ +// Copyright 2014 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 "script_injection_manager.h" + +#include +#include + +#include "base/auto_reset.h" +#include "base/bind.h" +#include "base/feature_list.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/values.h" +#include "base/logging.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_frame_observer.h" +#include "content/public/renderer/render_thread.h" +#include "extension_frame_helper.h" +#include "../common/host_id.h" +#include "script_injection.h" +#include "scripts_run_info.h" +#include "web_ui_injection_host.h" +#include "ipc/ipc_message_macros.h" +#include "third_party/blink/public/platform/web_url_error.h" +#include "third_party/blink/public/web/web_document.h" +#include "third_party/blink/public/web/web_frame.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_view.h" +#include "url/gurl.h" +#include "../common/user_scripts_features.h" + +namespace user_scripts { + +namespace { + +// The length of time to wait after the DOM is complete to try and run user +// scripts. +const int kScriptIdleTimeoutInMs = 200; + +// Returns the RunLocation that follows |run_location|. +UserScript::RunLocation NextRunLocation(UserScript::RunLocation run_location) { + switch (run_location) { + case UserScript::DOCUMENT_START: + return UserScript::DOCUMENT_END; + case UserScript::DOCUMENT_END: + return UserScript::DOCUMENT_IDLE; + case UserScript::DOCUMENT_IDLE: + return UserScript::RUN_LOCATION_LAST; + case UserScript::UNDEFINED: + case UserScript::RUN_DEFERRED: + case UserScript::BROWSER_DRIVEN: + case UserScript::RUN_LOCATION_LAST: + break; + } + NOTREACHED(); + return UserScript::RUN_LOCATION_LAST; +} + +} // namespace + +class ScriptInjectionManager::RFOHelper : public content::RenderFrameObserver { + public: + RFOHelper(content::RenderFrame* render_frame, + ScriptInjectionManager* manager); + ~RFOHelper() override; + + // commit @9f2aac4 + void Initialize(); + + private: + // RenderFrameObserver implementation. + void DidCreateNewDocument() override; + void DidCreateDocumentElement() override; + void DidFailProvisionalLoad() override; + void DidFinishDocumentLoad() override; + void WillDetach() override; + void OnDestruct() override; + void OnStop() override; + + // Tells the ScriptInjectionManager to run tasks associated with + // document_idle. + void RunIdle(); + + void StartInjectScripts(UserScript::RunLocation run_location); + + // Indicate that the frame is no longer valid because it is starting + // a new load or closing. + void InvalidateAndResetFrame(bool force_reset); + + // The owning ScriptInjectionManager. + ScriptInjectionManager* manager_; + + bool should_run_idle_ = true; // commit @9f2aac4 + + base::WeakPtrFactory weak_factory_{this}; +}; + +ScriptInjectionManager::RFOHelper::RFOHelper(content::RenderFrame* render_frame, + ScriptInjectionManager* manager) + : content::RenderFrameObserver(render_frame), + manager_(manager), + should_run_idle_(true) {} + +ScriptInjectionManager::RFOHelper::~RFOHelper() { +} + + +void ScriptInjectionManager::RFOHelper::Initialize() { + // Set up for the initial empty document, for which the Document created + // events do not happen as it's already present. + DidCreateNewDocument(); + // The initial empty document for a main frame may have scripts attached to it + // but we do not want to invalidate the frame and lose them when the next + // document loads. For example the IncognitoApiTest.IncognitoSplitMode test + // does `chrome.tabs.create()` with a script to be run, which is added to the + // frame before it navigates, so it needs to be preserved. However scripts in + // child frames are expected to be run inside the initial empty document. For + // example the ExecuteScriptApiTest.FrameWithHttp204 test creates a child + // frame at about:blank and expects to run injected scripts inside it. + // This is all quite inconsistent however tests both depend on us queuing and + // not queueing the DOCUMENT_START events in the initial empty document. + if (!render_frame()->IsMainFrame()) { + DidCreateDocumentElement(); + } +} + +void ScriptInjectionManager::RFOHelper::DidCreateNewDocument() { + // A new document is going to be shown, so invalidate the old document state. + // Don't force-reset the frame, because it is possible that a script injection + // was scheduled before the page was loaded, e.g. by navigating to a + // javascript: URL before the page has loaded. + constexpr bool kForceReset = false; + InvalidateAndResetFrame(kForceReset); +} + +void ScriptInjectionManager::RFOHelper::DidCreateDocumentElement() { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: DidCreateDocumentElement -> DOCUMENT_START"; + + ExtensionFrameHelper::Get(render_frame()) + ->ScheduleAtDocumentStart( + base::BindOnce(&ScriptInjectionManager::RFOHelper::StartInjectScripts, + weak_factory_.GetWeakPtr(), UserScript::DOCUMENT_START)); +} + +void ScriptInjectionManager::RFOHelper::DidFailProvisionalLoad() { + auto it = manager_->frame_statuses_.find(render_frame()); + if (it != manager_->frame_statuses_.end() && + it->second == UserScript::DOCUMENT_START) { + // Since the provisional load failed, the frame stays at its previous loaded + // state and origin (or the parent's origin for new/about:blank frames). + // Reset the frame to DOCUMENT_IDLE in order to reflect that the frame is + // done loading, and avoid any deadlock in the system. + // + // We skip injection of DOCUMENT_END and DOCUMENT_IDLE scripts, because the + // injections closely follow the DOMContentLoaded (and onload) events, which + // are not triggered after a failed provisional load. + // This assumption is verified in the checkDOMContentLoadedEvent subtest of + // ExecuteScriptApiTest.FrameWithHttp204 (browser_tests). + constexpr bool kForceReset = true; + InvalidateAndResetFrame(kForceReset); + should_run_idle_ = false; + manager_->frame_statuses_[render_frame()] = UserScript::DOCUMENT_IDLE; + } +} + +void ScriptInjectionManager::RFOHelper::DidFinishDocumentLoad() { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: DidFinishDocumentLoad -> DOCUMENT_END"; + + DCHECK(content::RenderThread::Get()); + ExtensionFrameHelper::Get(render_frame()) + ->ScheduleAtDocumentEnd( + base::BindOnce(&ScriptInjectionManager::RFOHelper::StartInjectScripts, + weak_factory_.GetWeakPtr(), UserScript::DOCUMENT_END)); + + // We try to run idle in two places: a delayed task here and in response to + // ContentRendererClient::RunScriptsAtDocumentIdle(). DidFinishDocumentLoad() + // corresponds to completing the document's load, whereas + // RunScriptsAtDocumentIdle() corresponds to completing the document and all + // subresources' load (but before the window.onload event). We don't want to + // hold up script injection for a particularly slow subresource, so we set a + // delayed task from here - but if we finish everything before that point + // (i.e., RunScriptsAtDocumentIdle() is triggered), then there's no reason to + // keep waiting. + render_frame() + ->GetTaskRunner(blink::TaskType::kInternalDefault) + ->PostDelayedTask( + FROM_HERE, + base::BindOnce(&ScriptInjectionManager::RFOHelper::RunIdle, + weak_factory_.GetWeakPtr()), + base::TimeDelta::FromMilliseconds(kScriptIdleTimeoutInMs)); + + ExtensionFrameHelper::Get(render_frame()) + ->ScheduleAtDocumentIdle( + base::BindOnce(&ScriptInjectionManager::RFOHelper::RunIdle, + weak_factory_.GetWeakPtr())); +} + +void ScriptInjectionManager::RFOHelper::WillDetach() { + // The frame is closing - invalidate. + constexpr bool kForceReset = true; + InvalidateAndResetFrame(kForceReset); +} + +void ScriptInjectionManager::RFOHelper::OnDestruct() { + manager_->RemoveObserver(this); +} + +void ScriptInjectionManager::RFOHelper::OnStop() { + // If the navigation request fails (e.g. 204/205/downloads), notify the + // extension to avoid keeping the frame in a START state indefinitely which + // leads to deadlocks. + DidFailProvisionalLoad(); +} + +void ScriptInjectionManager::RFOHelper::RunIdle() { + // Only notify the manager if the frame hasn't already had idle run since the + // task to RunIdle() was posted. + if (should_run_idle_) { + should_run_idle_ = false; + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: RunIdle -> DOCUMENT_IDLE"; + manager_->StartInjectScripts(render_frame(), UserScript::DOCUMENT_IDLE); + } +} + +void ScriptInjectionManager::RFOHelper::StartInjectScripts( + UserScript::RunLocation run_location) { + manager_->StartInjectScripts(render_frame(), run_location); +} + +void ScriptInjectionManager::RFOHelper::InvalidateAndResetFrame( + bool force_reset) { + // Invalidate any pending idle injections, and reset the frame inject on idle. + weak_factory_.InvalidateWeakPtrs(); + // We reset to inject on idle, because the frame can be reused (in the case of + // navigation). + should_run_idle_ = true; + + // Reset the frame if either |force_reset| is true, or if the manager is + // keeping track of the state of the frame (in which case we need to clean it + // up). + if (force_reset || manager_->frame_statuses_.count(render_frame()) != 0) + manager_->InvalidateForFrame(render_frame()); +} + +ScriptInjectionManager::ScriptInjectionManager( + UserScriptSetManager* user_script_set_manager) + : user_script_set_manager_(user_script_set_manager), + user_script_set_manager_observation_(this) { + user_script_set_manager_observation_.Observe(user_script_set_manager_); +} + +ScriptInjectionManager::~ScriptInjectionManager() { + for (const auto& injection : pending_injections_) + injection->invalidate_render_frame(); + for (const auto& injection : running_injections_) + injection->invalidate_render_frame(); +} + +void ScriptInjectionManager::OnRenderFrameCreated( + content::RenderFrame* render_frame) { + rfo_helpers_.push_back(std::make_unique(render_frame, this)); + rfo_helpers_.back()->Initialize(); // commit @9f2aac4 +} + +void ScriptInjectionManager::OnInjectionFinished( + ScriptInjection* injection) { + auto iter = + std::find_if(running_injections_.begin(), running_injections_.end(), + [injection](const std::unique_ptr& mode) { + return injection == mode.get(); + }); + if (iter != running_injections_.end()) + running_injections_.erase(iter); +} + +void ScriptInjectionManager::OnUserScriptsUpdated( + const std::set& changed_hosts) { + for (auto iter = pending_injections_.begin(); + iter != pending_injections_.end();) { + if (changed_hosts.count((*iter)->host_id()) > 0) + iter = pending_injections_.erase(iter); + else + ++iter; + } +} + +void ScriptInjectionManager::RemoveObserver(RFOHelper* helper) { + for (auto iter = rfo_helpers_.begin(); iter != rfo_helpers_.end(); ++iter) { + if (iter->get() == helper) { + rfo_helpers_.erase(iter); + break; + } + } +} + +void ScriptInjectionManager::InvalidateForFrame(content::RenderFrame* frame) { + // If the frame invalidated is the frame being injected into, we need to + // note it. + active_injection_frames_.erase(frame); + + for (auto iter = pending_injections_.begin(); + iter != pending_injections_.end();) { + if ((*iter)->render_frame() == frame) + iter = pending_injections_.erase(iter); + else + ++iter; + } + + frame_statuses_.erase(frame); +} + +void ScriptInjectionManager::StartInjectScripts( + content::RenderFrame* frame, + UserScript::RunLocation run_location) { + auto iter = frame_statuses_.find(frame); + // We also don't execute if we detect that the run location is somehow out of + // order. This can happen if: + // - The first run location reported for the frame isn't DOCUMENT_START, or + // - The run location reported doesn't immediately follow the previous + // reported run location. + // We don't want to run because extensions may have requirements that scripts + // running in an earlier run location have run by the time a later script + // runs. Better to just not run. + // Note that we check run_location > NextRunLocation() in the second clause + // (as opposed to !=) because earlier signals (like DidCreateDocumentElement) + // can happen multiple times, so we can receive earlier/equal run locations. + if ((iter == frame_statuses_.end() && + run_location != UserScript::DOCUMENT_START) || + (iter != frame_statuses_.end() && + run_location > NextRunLocation(iter->second))) { + // We also invalidate the frame, because the run order of pending injections + // may also be bad. + InvalidateForFrame(frame); + return; + } else if (iter != frame_statuses_.end() && iter->second >= run_location) { + // Certain run location signals (like DidCreateDocumentElement) can happen + // multiple times. Ignore the subsequent signals. + return; + } + + // Otherwise, all is right in the world, and we can get on with the + // injections! + frame_statuses_[frame] = run_location; + InjectScripts(frame, run_location); +} + +void ScriptInjectionManager::InjectScripts( + content::RenderFrame* frame, + UserScript::RunLocation run_location) { + // Find any injections that want to run on the given frame. + ScriptInjectionVector frame_injections; + for (auto iter = pending_injections_.begin(); + iter != pending_injections_.end();) { + if ((*iter)->render_frame() == frame) { + frame_injections.push_back(std::move(*iter)); + iter = pending_injections_.erase(iter); + } else { + ++iter; + } + } + + // Add any injections for user scripts. + int tab_id = ExtensionFrameHelper::Get(frame)->tab_id(); + user_script_set_manager_->GetAllInjections(&frame_injections, frame, tab_id, + run_location); + + // Note that we are running in |frame|. + active_injection_frames_.insert(frame); + + ScriptsRunInfo scripts_run_info(frame, run_location); + + for (auto iter = frame_injections.begin(); iter != frame_injections.end();) { + // It's possible for thScriptsRunInfoe frame to be invalidated in the course of injection + // (if a script removes its own frame, for example). If this happens, abort. + if (!active_injection_frames_.count(frame)) + break; + std::unique_ptr injection(std::move(*iter)); + iter = frame_injections.erase(iter); + TryToInject(std::move(injection), run_location, &scripts_run_info); + } + + // We are done running in the frame. + active_injection_frames_.erase(frame); + + scripts_run_info.LogRun(activity_logging_enabled_); +} + +void ScriptInjectionManager::TryToInject( + std::unique_ptr injection, + UserScript::RunLocation run_location, + ScriptsRunInfo* scripts_run_info) { + // Try to inject the script. If the injection is waiting (i.e., for + // permission), add it to the list of pending injections. If the injection + // has blocked, add it to the list of running injections. + // The Unretained below is safe because this object owns all the + // ScriptInjections, so is guaranteed to outlive them. + switch (injection->TryToInject( + run_location, scripts_run_info, + base::BindOnce(&ScriptInjectionManager::OnInjectionFinished, + base::Unretained(this)))) { + case ScriptInjection::INJECTION_WAITING: + pending_injections_.push_back(std::move(injection)); + break; + case ScriptInjection::INJECTION_BLOCKED: + running_injections_.push_back(std::move(injection)); + break; + case ScriptInjection::INJECTION_FINISHED: + break; + } +} + +} // namespace extensions diff --git a/components/user_scripts/renderer/script_injection_manager.h b/components/user_scripts/renderer/script_injection_manager.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/script_injection_manager.h @@ -0,0 +1,102 @@ +#include + +#include +#include +#include +#include + +#include "base/callback.h" +#include "base/macros.h" +#include "base/scoped_observation.h" +#include "../common/user_script.h" +#include "script_injection.h" +#include "user_script_set_manager.h" + +namespace user_scripts { + +// The ScriptInjectionManager manages extensions injecting scripts into frames +// via both content/user scripts and tabs.executeScript(). It is responsible for +// maintaining any pending injections awaiting permission or the appropriate +// load point, and injecting them when ready. +class ScriptInjectionManager : public UserScriptSetManager::Observer { + public: + explicit ScriptInjectionManager( + UserScriptSetManager* user_script_set_manager); + virtual ~ScriptInjectionManager(); + + // Notifies that a new render view has been created. + void OnRenderFrameCreated(content::RenderFrame* render_frame); + + // Removes pending injections of the unloaded extension. + //void OnExtensionUnloaded(const std::string& extension_id); + + void set_activity_logging_enabled(bool enabled) { + activity_logging_enabled_ = enabled; + } + + private: + // A RenderFrameObserver implementation which watches the various render + // frames in order to notify the ScriptInjectionManager of different + // document load states and IPCs. + class RFOHelper; + + using FrameStatusMap = + std::map; + + using ScriptInjectionVector = std::vector>; + + // Notifies that an injection has been finished. + void OnInjectionFinished(ScriptInjection* injection); + + // UserScriptSetManager::Observer implementation. + void OnUserScriptsUpdated(const std::set& changed_hosts) override; + + // Notifies that an RFOHelper should be removed. + void RemoveObserver(RFOHelper* helper); + + // Invalidate any pending tasks associated with |frame|. + void InvalidateForFrame(content::RenderFrame* frame); + + // Starts the process to inject appropriate scripts into |frame|. + void StartInjectScripts(content::RenderFrame* frame, + UserScript::RunLocation run_location); + + // Actually injects the scripts into |frame|. + void InjectScripts(content::RenderFrame* frame, + UserScript::RunLocation run_location); + + // Try to inject and store injection if it has not finished. + void TryToInject(std::unique_ptr injection, + UserScript::RunLocation run_location, + ScriptsRunInfo* scripts_run_info); + + // The map of active web frames to their corresponding statuses. The + // RunLocation of the frame corresponds to the last location that has ran. + FrameStatusMap frame_statuses_; + + // The frames currently being injected into, so long as that frame is valid. + std::set active_injection_frames_; + + // The collection of RFOHelpers. + std::vector> rfo_helpers_; + + // The set of UserScripts associated with extensions. Owned by the Dispatcher. + UserScriptSetManager* user_script_set_manager_; + + // Pending injections which are waiting for either the proper run location or + // user consent. + ScriptInjectionVector pending_injections_; + + // Running injections which are waiting for async callbacks from blink. + ScriptInjectionVector running_injections_; + + // Whether or not dom activity should be logged for scripts injected. + bool activity_logging_enabled_ = false; + + base::ScopedObservation + user_script_set_manager_observation_{this}; + + DISALLOW_COPY_AND_ASSIGN(ScriptInjectionManager); +}; + +} diff --git a/components/user_scripts/renderer/script_injector.h b/components/user_scripts/renderer/script_injector.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/script_injector.h @@ -0,0 +1,96 @@ +// Copyright 2014 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 USERSCRIPTS_RENDERER_SCRIPT_INJECTOR_H_ +#define USERSCRIPTS_RENDERER_SCRIPT_INJECTOR_H_ + +#include +#include + +#include "../common/constants.h" +#include "../common/user_script.h" +#include "third_party/blink/public/web/web_script_source.h" + +class InjectionHost; + +namespace blink { +class WebLocalFrame; +} + +namespace user_scripts { + +// The pseudo-delegate class for a ScriptInjection that provides all necessary +// information about how to inject the script, including what code to inject and +// when (run location), but without any injection logic. +class ScriptInjector { + public: + // The possible reasons for not injecting the script. + enum InjectFailureReason { + EXTENSION_REMOVED, // The extension was removed before injection. + NOT_ALLOWED, // The script is not allowed to inject. + WONT_INJECT // The injection won't inject because the user rejected + // (or just did not accept) the injection. + }; + + virtual ~ScriptInjector() {} + + // Returns the script type of this particular injection. + virtual UserScript::InjectionType script_type() const = 0; + + // Returns true if the script is running inside a user gesture. + virtual bool IsUserGesture() const = 0; + + // Returns the CSS origin of this injection. + virtual absl::optional GetCssOrigin() const = 0; + + // Returns the key for this injection, if it's a CSS injection. + virtual const absl::optional GetInjectionKey() const = 0; + + // Returns true if the script expects results. + virtual bool ExpectsResults() const = 0; + + // Returns true if the script should inject JS source at the given + // |run_location|. + virtual bool ShouldInjectJs( + UserScript::RunLocation run_location, + const std::set& executing_scripts) const = 0; + + // Returns true if the script should inject CSS at the given |run_location|. + virtual bool ShouldInjectCss( + UserScript::RunLocation run_location, + const std::set& injected_stylesheets) const = 0; + + // Returns the javascript sources to inject at the given |run_location|. + // Only called if ShouldInjectJs() is true. + virtual std::vector GetJsSources( + UserScript::RunLocation run_location, + std::set* executing_scripts, + size_t* num_injected_js_scripts) const = 0; + + // Returns the css to inject at the given |run_location|. + // Only called if ShouldInjectCss() is true. + virtual std::vector GetCssSources( + UserScript::RunLocation run_location, + std::set* injected_stylesheets, + size_t* num_injected_stylesheets) const = 0; + + // Notifies the script that injection has completed, with a possibly-populated + // list of results (depending on whether or not ExpectsResults() was true). + // |render_frame| contains the render frame, or null if the frame was + // invalidated. + virtual void OnInjectionComplete( + std::unique_ptr execution_result, + UserScript::RunLocation run_location, + content::RenderFrame* render_frame) = 0; + + // Notifies the script that injection will never occur. + // |render_frame| contains the render frame, or null if the frame was + // invalidated. + virtual void OnWillNotInject(InjectFailureReason reason, + content::RenderFrame* render_frame) = 0; +}; + +} // namespace extensions + +#endif // USERSCRIPTS_RENDERER_SCRIPT_INJECTOR_H_ diff --git a/components/user_scripts/renderer/scripts_run_info.cc b/components/user_scripts/renderer/scripts_run_info.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/scripts_run_info.cc @@ -0,0 +1,31 @@ +// Copyright 2014 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 "scripts_run_info.h" + +#include "base/metrics/histogram_macros.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "script_context.h" +#include "third_party/blink/public/web/web_local_frame.h" + +namespace user_scripts { + +ScriptsRunInfo::ScriptsRunInfo(content::RenderFrame* render_frame, + UserScript::RunLocation location) + : num_css(0u), + num_js(0u), + num_blocking_js(0u), + routing_id_(render_frame->GetRoutingID()), + run_location_(location), + frame_url_(ScriptContext::GetDocumentLoaderURLForFrame( + render_frame->GetWebFrame())) {} + +ScriptsRunInfo::~ScriptsRunInfo() { +} + +void ScriptsRunInfo::LogRun(bool send_script_activity) { +} + +} // namespace extensions diff --git a/components/user_scripts/renderer/scripts_run_info.h b/components/user_scripts/renderer/scripts_run_info.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/scripts_run_info.h @@ -0,0 +1,70 @@ +// Copyright 2014 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 USERSCRIPTS_RENDERER_SCRIPTS_RUN_INFO_H_ +#define USERSCRIPTS_RENDERER_SCRIPTS_RUN_INFO_H_ + +#include + +#include +#include +#include + +#include "base/macros.h" +#include "base/timer/elapsed_timer.h" +#include "../common/user_script.h" + +namespace content { +class RenderFrame; +} + +namespace user_scripts { + +// A struct containing information about a script run. +struct ScriptsRunInfo { + // Map of extensions IDs to the executing script paths. + typedef std::map > ExecutingScriptsMap; + + ScriptsRunInfo(content::RenderFrame* render_frame, + UserScript::RunLocation location); + ~ScriptsRunInfo(); + + // The number of CSS scripts injected. + size_t num_css; + // The number of JS scripts injected. + size_t num_js; + // The number of blocked JS scripts injected. + size_t num_blocking_js; + // A map of extension ids to executing script paths. + ExecutingScriptsMap executing_scripts; + // A map of extension ids to injected stylesheet paths. + ExecutingScriptsMap injected_stylesheets; + // The elapsed time since the ScriptsRunInfo was constructed. + base::ElapsedTimer timer; + + // Log information about a given script run. If |send_script_activity| is + // true, this also informs the browser of the script run. + void LogRun(bool send_script_activity); + + static void LogLongInjectionTaskTime(UserScript::RunLocation run_location, + const base::TimeDelta& elapsed); + + private: + // The routinig id to use to notify the browser of any injections. Since the + // frame may be deleted in injection, we don't hold on to a reference to it + // directly. + int routing_id_; + + // The run location at which injection is happening. + UserScript::RunLocation run_location_; + + // The url of the frame, preserved for the same reason as the routing id. + GURL frame_url_; + + DISALLOW_COPY_AND_ASSIGN(ScriptsRunInfo); +}; + +} // namespace extensions + +#endif // USERSCRIPTS_RENDERER_SCRIPTS_RUN_INFO_H_ diff --git a/components/user_scripts/renderer/user_script_injector.cc b/components/user_scripts/renderer/user_script_injector.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/user_script_injector.cc @@ -0,0 +1,228 @@ +// Copyright 2014 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 "user_script_injector.h" + +#include +#include + +#include "base/logging.h" +#include "base/lazy_instance.h" +#include "content/public/common/url_constants.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "content/public/renderer/render_view.h" +#include "components/user_scripts/renderer/grit/user_scripts_renderer_resources.h" +#include "injection_host.h" +#include "script_context.h" +#include "scripts_run_info.h" +#include "third_party/blink/public/web/web_document.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_script_source.h" +#include "ui/base/resource/resource_bundle.h" +#include "url/gurl.h" + +namespace user_scripts { + +namespace { + +struct RoutingInfoKey { + int routing_id; + int script_id; + + RoutingInfoKey(int routing_id, int script_id) + : routing_id(routing_id), script_id(script_id) {} + + bool operator<(const RoutingInfoKey& other) const { + return std::tie(routing_id, script_id) < + std::tie(other.routing_id, other.script_id); + } +}; + +using RoutingInfoMap = std::map; + +// A map records whether a given |script_id| from a webview-added user script +// is allowed to inject on the render of given |routing_id|. +// Once a script is added, the decision of whether or not allowed to inject +// won't be changed. +// After removed by the webview, the user scipt will also be removed +// from the render. Therefore, there won't be any query from the same +// |script_id| and |routing_id| pair. +// base::LazyInstance::DestructorAtExit g_routing_info_map = +// LAZY_INSTANCE_INITIALIZER; + +// Greasemonkey API source that is injected with the scripts. +struct GreasemonkeyApiJsString { + GreasemonkeyApiJsString(); + blink::WebScriptSource GetSource() const; + + private: + blink::WebString source_; +}; + +// The below constructor, monstrous as it is, just makes a WebScriptSource from +// the GreasemonkeyApiJs resource. +GreasemonkeyApiJsString::GreasemonkeyApiJsString() { + std::string greasemonky_api_js( + ui::ResourceBundle::GetSharedInstance().LoadDataResourceString( + IDR_GREASEMONKEY_API_JS)); + source_ = blink::WebString::FromUTF8(greasemonky_api_js); +} + +blink::WebScriptSource GreasemonkeyApiJsString::GetSource() const { + return blink::WebScriptSource(source_); +} + +base::LazyInstance::Leaky g_greasemonkey_api = + LAZY_INSTANCE_INITIALIZER; + +bool ShouldInjectScripts(const UserScript::FileList& scripts, + const std::set& injected_files) { + for (const std::unique_ptr& file : scripts) { + // Check if the script is already injected. + if (injected_files.count(file->url().path()) == 0) { + return true; + } + } + return false; +} + +} // namespace + +UserScriptInjector::UserScriptInjector(const UserScript* script, + UserScriptSet* script_list) + : script_(script), + user_script_set_(script_list), + script_id_(script_->id()), + user_script_set_observer_(this) { + user_script_set_observer_.Observe(script_list); +} + +UserScriptInjector::~UserScriptInjector() { +} + +void UserScriptInjector::OnUserScriptsUpdated( + const std::set& changed_hosts, + const UserScriptList& scripts) { + // When user scripts are updated, all the old script pointers are invalidated. + script_ = nullptr; + // If the host causing this injection changed, then this injection + // will be removed, and there's no guarantee the backing script still exists. + // if (changed_hosts.count(host_id_) > 0) + // return; + + for (const std::unique_ptr& script : scripts) { + if (script->id() == script_id_) { + script_ = script.get(); + break; + } + } + // If |host_id_| wasn't in |changed_hosts|, then the script for this injection + // should be guaranteed to exist. + DCHECK(script_); +} + +UserScript::InjectionType UserScriptInjector::script_type() const { + return UserScript::CONTENT_SCRIPT; +} + +bool UserScriptInjector::IsUserGesture() const { + return false; +} + +bool UserScriptInjector::ExpectsResults() const { + return false; +} + +absl::optional UserScriptInjector::GetCssOrigin() const { + return absl::nullopt; +} + +const absl::optional UserScriptInjector::GetInjectionKey() const { + return absl::nullopt; +} + +bool UserScriptInjector::ShouldInjectJs( + UserScript::RunLocation run_location, + const std::set& executing_scripts) const { + return script_ && script_->run_location() == run_location && + !script_->js_scripts().empty() && + ShouldInjectScripts(script_->js_scripts(), executing_scripts); +} + +bool UserScriptInjector::ShouldInjectCss( + UserScript::RunLocation run_location, + const std::set& injected_stylesheets) const { + return script_ && run_location == UserScript::DOCUMENT_START && + !script_->css_scripts().empty() && + ShouldInjectScripts(script_->css_scripts(), injected_stylesheets); +} + +std::vector UserScriptInjector::GetJsSources( + UserScript::RunLocation run_location, + std::set* executing_scripts, + size_t* num_injected_js_scripts) const { + DCHECK(script_); + std::vector sources; + + DCHECK_EQ(script_->run_location(), run_location); + + const UserScript::FileList& js_scripts = script_->js_scripts(); + sources.reserve(js_scripts.size() + + (script_->emulate_greasemonkey() ? 1 : 0)); + // Emulate Greasemonkey API for scripts that were converted to extension + // user scripts. + if (script_->emulate_greasemonkey()) + sources.push_back(g_greasemonkey_api.Get().GetSource()); + for (const std::unique_ptr& file : js_scripts) { + const GURL& script_url = file->url(); + // Check if the script is already injected. + if (executing_scripts->count(script_url.path()) != 0) + continue; + + sources.push_back(blink::WebScriptSource( + user_script_set_->GetJsSource(*file, script_->emulate_greasemonkey()), + script_url)); + + (*num_injected_js_scripts) += 1; + executing_scripts->insert(script_url.path()); + } + + return sources; +} + +std::vector UserScriptInjector::GetCssSources( + UserScript::RunLocation run_location, + std::set* injected_stylesheets, + size_t* num_injected_stylesheets) const { + DCHECK(script_); + DCHECK_EQ(UserScript::DOCUMENT_START, run_location); + + std::vector sources; + + const UserScript::FileList& css_scripts = script_->css_scripts(); + sources.reserve(css_scripts.size()); + for (const std::unique_ptr& file : script_->css_scripts()) { + const std::string& stylesheet_path = file->url().path(); + // Check if the stylesheet is already injected. + if (injected_stylesheets->count(stylesheet_path) != 0) + continue; + + sources.push_back(user_script_set_->GetCssSource(*file)); + (*num_injected_stylesheets) += 1; + injected_stylesheets->insert(stylesheet_path); + } + return sources; +} + +void UserScriptInjector::OnInjectionComplete( + std::unique_ptr execution_result, + UserScript::RunLocation run_location, + content::RenderFrame* render_frame) {} + +void UserScriptInjector::OnWillNotInject(InjectFailureReason reason, + content::RenderFrame* render_frame) { +} + +} // namespace extensions diff --git a/components/user_scripts/renderer/user_script_injector.h b/components/user_scripts/renderer/user_script_injector.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/user_script_injector.h @@ -0,0 +1,87 @@ +// Copyright 2014 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 USERSCRIPTS_RENDERER_USER_SCRIPT_INJECTOR_H_ +#define USERSCRIPTS_RENDERER_USER_SCRIPT_INJECTOR_H_ + +#include +#include + +#include "base/macros.h" +#include "base/values.h" +#include "base/scoped_observation.h" +#include "../common/user_script.h" +#include "script_injection.h" +#include "user_script_set.h" + +class InjectionHost; + +namespace blink { +class WebLocalFrame; +} + +namespace user_scripts { + +// A ScriptInjector for UserScripts. +class UserScriptInjector : public ScriptInjector, + public UserScriptSet::Observer { + public: + UserScriptInjector(const UserScript* user_script, + UserScriptSet* user_script_set); + ~UserScriptInjector() override; + + private: + // UserScriptSet::Observer implementation. + void OnUserScriptsUpdated(const std::set& changed_hosts, + const UserScriptList& scripts) override; + + // ScriptInjector implementation. + UserScript::InjectionType script_type() const override; + bool IsUserGesture() const override; + absl::optional GetCssOrigin() const override; + const absl::optional GetInjectionKey() const override; + bool ExpectsResults() const override; + bool ShouldInjectJs( + UserScript::RunLocation run_location, + const std::set& executing_scripts) const override; + bool ShouldInjectCss( + UserScript::RunLocation run_location, + const std::set& injected_stylesheets) const override; + std::vector GetJsSources( + UserScript::RunLocation run_location, + std::set* executing_scripts, + size_t* num_injected_js_scripts) const override; + std::vector GetCssSources( + UserScript::RunLocation run_location, + std::set* injected_stylesheets, + size_t* num_injected_stylesheets) const override; + void OnInjectionComplete(std::unique_ptr execution_result, + UserScript::RunLocation run_location, + content::RenderFrame* render_frame) override; + void OnWillNotInject(InjectFailureReason reason, + content::RenderFrame* render_frame) override; + + // The associated user script. Owned by the UserScriptInjector that created + // this object. + const UserScript* script_; + + // The UserScriptSet that eventually owns the UserScript this + // UserScriptInjector points to. + // Outlives |this|. + UserScriptSet* const user_script_set_; + + // The id of the associated user script. We cache this because when we update + // the |script_| associated with this injection, the old referance may be + // deleted. + int script_id_; + + base::ScopedObservation + user_script_set_observer_{this}; + + DISALLOW_COPY_AND_ASSIGN(UserScriptInjector); +}; + +} // namespace extensions + +#endif // USERSCRIPTS_RENDERER_USER_SCRIPT_INJECTOR_H_ diff --git a/components/user_scripts/renderer/user_script_set.cc b/components/user_scripts/renderer/user_script_set.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/user_script_set.cc @@ -0,0 +1,259 @@ +// Copyright 2014 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 "user_script_set.h" + +#include + +#include + +#include "base/logging.h" +#include "base/debug/alias.h" +#include "base/memory/ref_counted.h" +#include "base/strings/strcat.h" +#include "content/public/common/url_constants.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "injection_host.h" +#include "script_context.h" +#include "script_injection.h" +#include "user_script_injector.h" +#include "web_ui_injection_host.h" +#include "third_party/blink/public/web/web_document.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "url/gurl.h" +#include "../common/user_scripts_features.h" + +namespace user_scripts { + +namespace { + +// These two strings are injected before and after the Greasemonkey API and +// user script to wrap it in an anonymous scope. +const char kUserScriptHead[] = "(function (unsafeWindow) {\n"; +const char kUserScriptTail[] = "\n})(window);"; +// Maximum number of total content scripts we allow (across all extensions). +// The limit exists to diagnose https://crbug.com/723381. The number is +// arbitrarily chosen. +// TODO(lazyboy): Remove when the bug is fixed. +const uint32_t kNumScriptsArbitraryMax = 100000u; + +GURL GetDocumentUrlForFrame(blink::WebLocalFrame* frame) { + GURL data_source_url = ScriptContext::GetDocumentLoaderURLForFrame(frame); + if (!data_source_url.is_empty() && frame->IsViewSourceModeEnabled()) { + data_source_url = GURL(content::kViewSourceScheme + std::string(":") + + data_source_url.spec()); + } + + return data_source_url; +} + +} // namespace + +UserScriptSet::UserScriptSet() {} + +UserScriptSet::~UserScriptSet() { +} + +void UserScriptSet::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void UserScriptSet::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +void UserScriptSet::GetInjections( + std::vector>* injections, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location, + bool log_activity) { + GURL document_url = GetDocumentUrlForFrame(render_frame->GetWebFrame()); + for (const std::unique_ptr& script : scripts_) { + std::unique_ptr injection = GetInjectionForScript( + script.get(), render_frame, tab_id, run_location, document_url, + /* is_declarative, */ log_activity); + if (injection.get()) + injections->push_back(std::move(injection)); + } +} + +bool UserScriptSet::UpdateUserScripts( + base::ReadOnlySharedMemoryRegion shared_memory, + const std::set& changed_hosts, + bool whitelisted_only) { + bool only_inject_incognito = false; + //ExtensionsRendererClient::Get()->IsIncognitoProcess(); + + // Create the shared memory mapping. + shared_memory_mapping_ = shared_memory.Map(); + if (!shared_memory.IsValid()) + return false; + + // First get the size of the memory block. + const base::Pickle::Header* pickle_header = + shared_memory_mapping_.GetMemoryAs(); + if (!pickle_header) + return false; + + // Now read in the rest of the block. + size_t pickle_size = + sizeof(base::Pickle::Header) + pickle_header->payload_size; + + // Unpickle scripts. + uint32_t num_scripts = 0; + auto memory = shared_memory_mapping_.GetMemoryAsSpan(pickle_size); + if (!memory.size()) + return false; + + base::Pickle pickle(memory.data(), pickle_size); + base::PickleIterator iter(pickle); + base::debug::Alias(&pickle_size); + CHECK(iter.ReadUInt32(&num_scripts)); + + // Sometimes the shared memory contents seem to be corrupted + // (https://crbug.com/723381). Set an arbitrary max limit to the number of + // scripts so that we don't add OOM noise to crash reports. + CHECK_LT(num_scripts, kNumScriptsArbitraryMax); + + scripts_.clear(); + script_sources_.clear(); + scripts_.reserve(num_scripts); + for (uint32_t i = 0; i < num_scripts; ++i) { + std::unique_ptr script(new UserScript()); + script->Unpickle(pickle, &iter); + + // Note that this is a pointer into shared memory. We don't own it. It gets + // cleared up when the last renderer or browser process drops their + // reference to the shared memory. + for (size_t j = 0; j < script->js_scripts().size(); ++j) { + const char* body = NULL; + int body_length = 0; + CHECK(iter.ReadData(&body, &body_length)); + script->js_scripts()[j]->set_external_content( + base::StringPiece(body, body_length)); + } + for (size_t j = 0; j < script->css_scripts().size(); ++j) { + const char* body = NULL; + int body_length = 0; + CHECK(iter.ReadData(&body, &body_length)); + script->css_scripts()[j]->set_external_content( + base::StringPiece(body, body_length)); + } + + if (only_inject_incognito && !script->is_incognito_enabled()) + continue; // This script shouldn't run in an incognito tab. + + scripts_.push_back(std::move(script)); + } + + for (auto& observer : observers_) + observer.OnUserScriptsUpdated(changed_hosts, scripts_); + return true; +} + +void UserScriptSet::AddScript(std::unique_ptr script) { + scripts_.push_back(std::move(script)); +} + +std::unique_ptr UserScriptSet::GetInjectionForScript( + const UserScript* script, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location, + const GURL& document_url, + //bool is_declarative, + bool log_activity) { + std::unique_ptr injection; + std::unique_ptr injection_host; + blink::WebLocalFrame* web_frame = render_frame->GetWebFrame(); + + const HostID& host_id = script->host_id(); + injection_host.reset(new WebUIInjectionHost(host_id)); + + GURL effective_document_url = + ScriptContext::GetEffectiveDocumentURLForInjection( + web_frame, document_url, script->match_origin_as_fallback()); + + bool is_subframe = web_frame->Parent(); + if (!script->MatchesDocument(effective_document_url, is_subframe)) { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: No Match name=" << script->name() << " " << + "url=" << effective_document_url.spec(); + return injection; + } + + std::unique_ptr injector( + new UserScriptInjector(script, this)); + + bool inject_css = !script->css_scripts().empty() && + run_location == UserScript::DOCUMENT_START; + bool inject_js = + !script->js_scripts().empty() && script->run_location() == run_location; + if (inject_css || inject_js) { + injection.reset(new ScriptInjection(std::move(injector), render_frame, + std::move(injection_host), run_location, + log_activity)); + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: Match name=" << script->name() << " " << + "url=" << effective_document_url.spec(); + } else { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) { + if (script->run_location() != run_location) + LOG(INFO) << "UserScripts: run location for name=" << script->name() << + "current " << run_location << " " << + "need " << script->run_location(); + else + LOG(INFO) << "UserScripts: Match but no run name=" << script->name() << " " << + "url=" << effective_document_url.spec(); + } + } + return injection; +} + +blink::WebString UserScriptSet::GetJsSource(const UserScript::File& file, + bool emulate_greasemonkey) { + const GURL& url = file.url(); + auto iter = script_sources_.find(url); + if (iter != script_sources_.end()) { + return iter->second; + } + + base::StringPiece script_content = file.GetContent(); + blink::WebString source; + if (emulate_greasemonkey) { + // We add this dumb function wrapper for user scripts to emulate what + // Greasemonkey does. |script_content| becomes: + // concat(kUserScriptHead, script_content, kUserScriptTail). + std::string content = + base::StrCat({kUserScriptHead, script_content, kUserScriptTail}); + source = blink::WebString::FromUTF8(content); + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: Injecting w/greasemonkey " << file.url(); + } else { + source = blink::WebString::FromUTF8(script_content.data(), + script_content.length()); + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: Injecting " << file.url(); + } + script_sources_[url] = source; + return source; +} + +blink::WebString UserScriptSet::GetCssSource(const UserScript::File& file) { + const GURL& url = file.url(); + auto iter = script_sources_.find(url); + if (iter != script_sources_.end()) + return iter->second; + + base::StringPiece script_content = file.GetContent(); + return script_sources_ + .insert(std::make_pair( + url, blink::WebString::FromUTF8(script_content.data(), + script_content.length()))) + .first->second; +} + +} // namespace extensions diff --git a/components/user_scripts/renderer/user_script_set.h b/components/user_scripts/renderer/user_script_set.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/user_script_set.h @@ -0,0 +1,102 @@ +// Copyright 2014 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 USERSCRIPTS_RENDERER_USER_SCRIPT_SET_H_ +#define USERSCRIPTS_RENDERER_USER_SCRIPT_SET_H_ + +#include +#include +#include +#include +#include + +#include "base/macros.h" +#include "base/memory/read_only_shared_memory_region.h" +#include "base/observer_list.h" +#include "../common/user_script.h" +#include "third_party/blink/public/platform/web_string.h" + +class GURL; + +namespace content { +class RenderFrame; +} + +namespace user_scripts { +class ScriptInjection; + +// The UserScriptSet is a collection of UserScripts which knows how to update +// itself from SharedMemory and create ScriptInjections for UserScripts to +// inject on a page. +class UserScriptSet { + public: + class Observer { + public: + // Called when the set of user scripts is updated. |changed_hosts| contains + // the hosts whose scripts have been altered. Note that *all* script objects + // are invalidated, even if they aren't in |changed_hosts|. + virtual void OnUserScriptsUpdated(const std::set& changed_hosts, + const UserScriptList& scripts) = 0; + }; + + UserScriptSet(); + ~UserScriptSet(); + + // Adds or removes observers. + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + void AddScript(std::unique_ptr script); + + // Append any ScriptInjections that should run on the given |render_frame| and + // |tab_id|, at the given |run_location|, to |injections|. + // |extensions| is passed in to verify the corresponding extension is still + // valid. + void GetInjections(std::vector>* injections, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location, + bool log_activity); + + // Updates scripts given the shared memory region containing user scripts. + // Returns true if the scripts were successfully updated. + bool UpdateUserScripts(base::ReadOnlySharedMemoryRegion shared_memory, + const std::set& changed_hosts, + bool whitelisted_only); + + // Returns the contents of a script file. + // Note that copying is cheap as this uses WebString. + blink::WebString GetJsSource(const UserScript::File& file, + bool emulate_greasemonkey); + blink::WebString GetCssSource(const UserScript::File& file); + + private: + // Returns a new ScriptInjection for the given |script| to execute in the + // |render_frame|, or NULL if the script should not execute. + std::unique_ptr GetInjectionForScript( + const UserScript* script, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location, + const GURL& document_url, + //bool is_declarative, + bool log_activity); + + // Shared memory mapping containing raw script data. + base::ReadOnlySharedMemoryMapping shared_memory_mapping_; + + // The UserScripts this injector manages. + UserScriptList scripts_; + + // Map of user script file url -> source. + std::map script_sources_; + + // The associated observers. + base::ObserverList::Unchecked observers_; + + DISALLOW_COPY_AND_ASSIGN(UserScriptSet); +}; + +} // namespace extensions + +#endif // USERSCRIPTS_RENDERER_USER_SCRIPT_SET_H_ diff --git a/components/user_scripts/renderer/user_script_set_manager.cc b/components/user_scripts/renderer/user_script_set_manager.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/user_script_set_manager.cc @@ -0,0 +1,77 @@ +#include "user_script_set_manager.h" + +#include "base/logging.h" +#include "content/public/renderer/render_thread.h" +#include "../common/host_id.h" +#include "../common/extension_messages.h" +#include "../common/user_scripts_features.h" +#include "user_script_set.h" + +namespace user_scripts { + +UserScriptSetManager::UserScriptSetManager() { + content::RenderThread::Get()->AddObserver(this); +} + +UserScriptSetManager::~UserScriptSetManager() { +} + +void UserScriptSetManager::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void UserScriptSetManager::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +bool UserScriptSetManager::OnControlMessageReceived( + const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(UserScriptSetManager, message) + IPC_MESSAGE_HANDLER(ExtensionMsg_UpdateUserScripts, OnUpdateUserScripts) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + return handled; +} + +void UserScriptSetManager::GetAllInjections( + std::vector>* injections, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location) { + + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: GetAllInjections"; + + // static_scripts_ is UserScriptSet + static_scripts_.GetInjections(injections, render_frame, tab_id, run_location, + activity_logging_enabled_); +} + +void UserScriptSetManager::OnUpdateUserScripts( + base::ReadOnlySharedMemoryRegion shared_memory) { + if (!shared_memory.IsValid()) { + NOTREACHED() << "Bad scripts handle"; + return; + } + + UserScriptSet* scripts = NULL; + scripts = &static_scripts_; + + DCHECK(scripts); + + // If no hosts are included in the set, that indicates that all + // hosts were updated. Add them all to the set so that observers and + // individual UserScriptSets don't need to know this detail. + //const std::set* effective_hosts = &changed_hosts; + std::set all_hosts; + const std::set* effective_hosts = &all_hosts; + + if (scripts->UpdateUserScripts(std::move(shared_memory), *effective_hosts, + false /*whitelisted_only*/)) { + for (auto& observer : observers_) + observer.OnUserScriptsUpdated(all_hosts /* *effective_hosts*/); + } +} + +} \ No newline at end of file diff --git a/components/user_scripts/renderer/user_script_set_manager.h b/components/user_scripts/renderer/user_script_set_manager.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/user_script_set_manager.h @@ -0,0 +1,62 @@ +#ifndef USERSCRIPTS_RENDER_SET_MANAGER_H_ +#define USERSCRIPTS_RENDER_SET_MANAGER_H_ + +#include +#include +#include +#include + +#include "base/macros.h" +#include "base/memory/read_only_shared_memory_region.h" +#include "base/observer_list.h" +#include "content/public/renderer/render_thread_observer.h" +#include "../common/host_id.h" +#include "user_script_set.h" +#include "script_injection.h" + +namespace user_scripts { + +class UserScriptSetManager : public content::RenderThreadObserver { + public: + class Observer { + public: + virtual void OnUserScriptsUpdated(const std::set& changed_hosts) = 0; + }; + + UserScriptSetManager(); + + ~UserScriptSetManager() override; + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Append all injections from |static_scripts| and each of + // |programmatic_scripts_| to |injections|. + void GetAllInjections( + std::vector>* injections, + content::RenderFrame* render_frame, + int tab_id, + UserScript::RunLocation run_location); + +private: + // content::RenderThreadObserver implementation. + bool OnControlMessageReceived(const IPC::Message& message) override; + + base::ObserverList::Unchecked observers_; + + // Handle the UpdateUserScripts extension message. + void OnUpdateUserScripts(base::ReadOnlySharedMemoryRegion shared_memory); + //, const HostID& host_id, + //const std::set& changed_hosts, + //bool whitelisted_only); + + // Scripts statically defined in extension manifests. + UserScriptSet static_scripts_; + + // Whether or not dom activity should be logged for scripts injected. + bool activity_logging_enabled_ = false; +}; + +} + +#endif \ No newline at end of file diff --git a/components/user_scripts/renderer/user_scripts_dispatcher.cc b/components/user_scripts/renderer/user_scripts_dispatcher.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/user_scripts_dispatcher.cc @@ -0,0 +1,36 @@ +#include "user_scripts_dispatcher.h" + +#include + +#include +#include +#include + +#include "content/public/renderer/render_thread.h" +#include "extension_frame_helper.h" + +namespace user_scripts { + +// ex ChromeExtensionsDispatcherDelegate +UserScriptsDispatcher::UserScriptsDispatcher() + : user_script_set_manager_observer_(this) { + user_script_set_manager_.reset(new UserScriptSetManager()); + script_injection_manager_.reset( + new ScriptInjectionManager(user_script_set_manager_.get())); + user_script_set_manager_observer_.Observe(user_script_set_manager_.get()); +} + +UserScriptsDispatcher::~UserScriptsDispatcher() { +} + +void UserScriptsDispatcher::OnRenderThreadStarted(content::RenderThread* thread) { +} + +void UserScriptsDispatcher::OnUserScriptsUpdated(const std::set& changed_hosts) { +} + +void UserScriptsDispatcher::OnRenderFrameCreated(content::RenderFrame* render_frame) { + script_injection_manager_->OnRenderFrameCreated(render_frame); +} + +} \ No newline at end of file diff --git a/components/user_scripts/renderer/user_scripts_dispatcher.h b/components/user_scripts/renderer/user_scripts_dispatcher.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/user_scripts_dispatcher.h @@ -0,0 +1,48 @@ +#ifndef USERSCRIPTS_RENDER_DISPATCHER_H_ +#define USERSCRIPTS_RENDER_DISPATCHER_H_ + +#include "user_script_set_manager.h" +#include "script_injection_manager.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "base/macros.h" +#include "base/scoped_observation.h" +#include "content/public/renderer/render_thread_observer.h" +#include "content/public/renderer/render_thread.h" +#include "../common/host_id.h" +#include "user_script_set_manager.h" +#include "script_injection.h" + +namespace user_scripts { + +class UserScriptsDispatcher : public content::RenderThreadObserver, + public UserScriptSetManager::Observer { + + public: + explicit UserScriptsDispatcher(); + ~UserScriptsDispatcher() override; + + void OnRenderThreadStarted(content::RenderThread* thread); + void OnUserScriptsUpdated(const std::set& changed_hosts) override; + void OnRenderFrameCreated(content::RenderFrame* render_frame); + + private: + std::unique_ptr user_script_set_manager_; + + std::unique_ptr script_injection_manager_; + + base::ScopedObservation + user_script_set_manager_observer_{this}; +}; + +} + +#endif \ No newline at end of file diff --git a/components/user_scripts/renderer/user_scripts_renderer_client.cc b/components/user_scripts/renderer/user_scripts_renderer_client.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/user_scripts_renderer_client.cc @@ -0,0 +1,105 @@ +#include "user_scripts_renderer_client.h" + +#include +#include + +#include "base/logging.h" +#include "base/lazy_instance.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_thread.h" +#include "content/public/renderer/render_frame_visitor.h" +#include "chrome/renderer/chrome_render_thread_observer.h" + +#include "../common/user_scripts_features.h" +#include "user_scripts_dispatcher.h" +#include "extension_frame_helper.h" + +namespace user_scripts { + +// was ChromeExtensionsRendererClient +UserScriptsRendererClient::UserScriptsRendererClient() {} + +UserScriptsRendererClient::~UserScriptsRendererClient() {} + +// static +UserScriptsRendererClient* UserScriptsRendererClient::GetInstance() { + static base::LazyInstance::Leaky client = + LAZY_INSTANCE_INITIALIZER; + return client.Pointer(); +} + +void UserScriptsRendererClient::RenderThreadStarted() { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: RenderThreadStarted"; + + content::RenderThread* thread = content::RenderThread::Get(); + dispatcher_ = std::make_unique(); + + dispatcher_->OnRenderThreadStarted(thread); + thread->AddObserver(dispatcher_.get()); +} + +void UserScriptsRendererClient::ConfigurationUpdated() { + if (base::FeatureList::IsEnabled(features::kEnableLoggingUserScripts)) + LOG(INFO) << "UserScripts: Configuration Updated"; + + struct WatchFrame : public content::RenderFrameVisitor { + bool Visit(content::RenderFrame* frame) override { + if (frame) + UserScriptsRendererClient::GetInstance()->RenderFrameCreated(frame, NULL); + return true; // Continue visiting. + } + }; + WatchFrame visitor = {}; + content::RenderFrame::ForEach(&visitor); +} + +void UserScriptsRendererClient::RenderFrameCreated( + content::RenderFrame* render_frame, + service_manager::BinderRegistry* registry) { + + auto params = ChromeRenderThreadObserver::GetDynamicParams(); + enabled_ = params.allow_userscript; + if (!enabled_) return; + + if (loaded_ == false) { + loaded_ = true; + new user_scripts::ExtensionFrameHelper(render_frame); + dispatcher_->OnRenderFrameCreated(render_frame); + } +} + +void UserScriptsRendererClient::RunScriptsAtDocumentStart(content::RenderFrame* render_frame) { + if (!enabled_ || !loaded_) return; + + ExtensionFrameHelper* frame_helper = ExtensionFrameHelper::Get(render_frame); + if (!frame_helper) + return; // The frame is invisible to user scripts. + + frame_helper->RunScriptsAtDocumentStart(); + // |frame_helper| and |render_frame| might be dead by now. +} + +void UserScriptsRendererClient::RunScriptsAtDocumentEnd(content::RenderFrame* render_frame) { + if (!enabled_ || !loaded_) return; + + ExtensionFrameHelper* frame_helper = ExtensionFrameHelper::Get(render_frame); + if (!frame_helper) + return; // The frame is invisible to user scripts. + + frame_helper->RunScriptsAtDocumentEnd(); + // |frame_helper| and |render_frame| might be dead by now. +} + +void UserScriptsRendererClient::RunScriptsAtDocumentIdle(content::RenderFrame* render_frame) { + if (!enabled_ || !loaded_) return; + + ExtensionFrameHelper* frame_helper = ExtensionFrameHelper::Get(render_frame); + if (!frame_helper) + return; // The frame is invisible to user scripts. + + frame_helper->RunScriptsAtDocumentIdle(); + // |frame_helper| and |render_frame| might be dead by now. +} + +} \ No newline at end of file diff --git a/components/user_scripts/renderer/user_scripts_renderer_client.h b/components/user_scripts/renderer/user_scripts_renderer_client.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/user_scripts_renderer_client.h @@ -0,0 +1,36 @@ +#ifndef USERSCRIPTS_RENDER_CLIENT_H_ +#define USERSCRIPTS_RENDER_CLIENT_H_ + +#include +#include + +#include "base/macros.h" +#include "user_scripts_dispatcher.h" +#include "services/service_manager/public/cpp/binder_registry.h" + +namespace user_scripts { + +class UserScriptsRendererClient { + public: + UserScriptsRendererClient(); + ~UserScriptsRendererClient(); + + static UserScriptsRendererClient* GetInstance(); + + void RenderThreadStarted(); + void ConfigurationUpdated(); + void RenderFrameCreated(content::RenderFrame* render_frame, + service_manager::BinderRegistry* registry); + void RunScriptsAtDocumentStart(content::RenderFrame* render_frame); + void RunScriptsAtDocumentEnd(content::RenderFrame* render_frame); + void RunScriptsAtDocumentIdle(content::RenderFrame* render_frame); + + private: + std::unique_ptr dispatcher_; + bool enabled_ = false; + bool loaded_ = false; +}; + +} + +#endif \ No newline at end of file diff --git a/components/user_scripts/renderer/web_ui_injection_host.cc b/components/user_scripts/renderer/web_ui_injection_host.cc new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/web_ui_injection_host.cc @@ -0,0 +1,40 @@ +// Copyright 2015 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 "web_ui_injection_host.h" +#include "base/no_destructor.h" + +namespace { + +// The default secure CSP to be used in order to prevent remote scripts. +const char kDefaultSecureCSP[] = "script-src 'self'; object-src 'self';"; + +} + +WebUIInjectionHost::WebUIInjectionHost(const HostID& host_id) + : InjectionHost(host_id), + url_(host_id.id()) { +} + +WebUIInjectionHost::~WebUIInjectionHost() { +} + +const std::string* WebUIInjectionHost::GetContentSecurityPolicy() const { + // Use the main world CSP. + // return nullptr; + + // The isolated world will use its own CSP which blocks remotely hosted + // code. + static const base::NoDestructor default_isolated_world_csp( + kDefaultSecureCSP); + return default_isolated_world_csp.get(); +} + +const GURL& WebUIInjectionHost::url() const { + return url_; +} + +const std::string& WebUIInjectionHost::name() const { + return id().id(); +} diff --git a/components/user_scripts/renderer/web_ui_injection_host.h b/components/user_scripts/renderer/web_ui_injection_host.h new file mode 100755 --- /dev/null +++ b/components/user_scripts/renderer/web_ui_injection_host.h @@ -0,0 +1,28 @@ +// Copyright 2015 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 USERSCRIPTS_RENDERER_WEB_UI_INJECTION_HOST_H_ +#define USERSCRIPTS_RENDERER_WEB_UI_INJECTION_HOST_H_ + +#include "base/macros.h" +#include "injection_host.h" + +class WebUIInjectionHost : public InjectionHost { + public: + WebUIInjectionHost(const HostID& host_id); + ~WebUIInjectionHost() override; + + private: + // InjectionHost: + const std::string* GetContentSecurityPolicy() const override; + const GURL& url() const override; + const std::string& name() const override; + + private: + GURL url_; + + DISALLOW_COPY_AND_ASSIGN(WebUIInjectionHost); +}; + +#endif // USERSCRIPTS_RENDERER_WEB_UI_INJECTION_HOST_H_ diff --git a/components/user_scripts/strings/userscripts_strings.grdp b/components/user_scripts/strings/userscripts_strings.grdp new file mode 100755 --- /dev/null +++ b/components/user_scripts/strings/userscripts_strings.grdp @@ -0,0 +1,55 @@ + + + + + + User Scripts + + + + Activate User Scripts + + + + ON + + + OFF + + + + Add script + + + Experimental support for Greasemonkey-style user scripts. + + + + Version: + + + File: + + + Url: + + + + View source + + + + Only install user scripts that you have verified are secure, user scripts can steal your credentials and data. + +Do you want to install %s? + + + Yes + + + No + + + \ No newline at end of file diff --git a/tools/gritsettings/resource_ids.spec b/tools/gritsettings/resource_ids.spec --- a/tools/gritsettings/resource_ids.spec +++ b/tools/gritsettings/resource_ids.spec @@ -556,6 +556,12 @@ "components/autofill/core/browser/autofill_address_rewriter_resources.grd":{ "includes": [2880] }, + "components/user_scripts/renderer/resources/user_scripts_renderer_resources.grd": { + "includes": [6000], + }, + "components/user_scripts/browser/resources/browser_resources.grd": { + "includes": [6020], + }, # END components/ section. # START ios/ section. -- 2.17.1