diff --git a/app/brave_settings_strings.grdp b/app/brave_settings_strings.grdp index b514780f1f20..bf54c4da2e08 100644 --- a/app/brave_settings_strings.grdp +++ b/app/brave_settings_strings.grdp @@ -453,6 +453,42 @@ Filter lists + + Custom scriptlets + + + Add new scriptlet + + + Add new scriptlet + + + Edit scriptlet + + + Name + + + Content + + + Cancel + + + Save + + + Are you sure you want to delete this scriptlet? If this scriptlet is used in any custom filter, it will no longer work. + + + Scriptlet is already exists. + + + Invalid scriptlet name. + + + Scriptlet is not found. + Shortcuts diff --git a/browser/brave_shields/ad_block_service_browsertest.cc b/browser/brave_shields/ad_block_service_browsertest.cc index 136e6b9d0801..4207ebf5ebf6 100644 --- a/browser/brave_shields/ad_block_service_browsertest.cc +++ b/browser/brave_shields/ad_block_service_browsertest.cc @@ -245,9 +245,7 @@ void AdBlockServiceTest::UpdateAdBlockResources(const std::string& resources) { brave_shields::AdBlockService* service = g_brave_browser_process->ad_block_service(); - static_cast( - service->resource_provider()) - ->OnComponentReady(component_path); + service->default_resource_provider()->OnComponentReady(component_path); } void AdBlockServiceTest::UpdateAdBlockInstanceWithRules( diff --git a/browser/resources/settings/default_brave_shields_page/brave_adblock_browser_proxy.ts b/browser/resources/settings/default_brave_shields_page/brave_adblock_browser_proxy.ts index df24e2c75d90..79aaebdd47aa 100644 --- a/browser/resources/settings/default_brave_shields_page/brave_adblock_browser_proxy.ts +++ b/browser/resources/settings/default_brave_shields_page/brave_adblock_browser_proxy.ts @@ -5,7 +5,22 @@ // @ts-nocheck TODO(petemill): Define types and remove ts-nocheck -import { sendWithPromise, addWebUiListener } from 'chrome://resources/js/cr.js'; +import { sendWithPromise, addWebUiListener } from 'chrome://resources/js/cr.js' + +export class Scriptlet { + name: string + kind: object = { + mime: 'application/javascript' + } + content: string +} + +export enum ErrorCode { + kOK = 0, + kInvalidName, + kAlreadyExists, + kNotFound, +} export interface BraveAdblockBrowserProxy { getRegionalLists(): Promise // TODO(petemill): Define the expected type @@ -19,6 +34,10 @@ export interface BraveAdblockBrowserProxy { updateSubscription(url: string) deleteSubscription(url: string) viewSubscription(url: string) + getCustomScriptlets(): Promise + addCustomScriptlet(scriptlet: Scriptlet): Promise + updateCustomScriptlet(name: string, scriptlet: Scriptlet): Promise + removeCustomScriptlet(name: string): Promise } export class BraveAdblockBrowserProxyImpl implements BraveAdblockBrowserProxy { @@ -74,7 +93,53 @@ export class BraveAdblockBrowserProxyImpl implements BraveAdblockBrowserProxy { chrome.send('brave_adblock.viewSubscription', [url]) } - addWebUiListener (event_name, callback) { + utf8ToBase64_(str) { + const uint8Array = new TextEncoder().encode(str) + const base64String = btoa(String.fromCharCode.apply(null, uint8Array)) + return base64String + } + + base64ToUtf8_(base64) { + const binaryString = atob(base64) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + return new TextDecoder().decode(bytes) + } + + getCustomScriptlets() { + return sendWithPromise('brave_adblock.getCustomScriptlets') + .then((scriptlets) => { + for (const scriptlet of scriptlets) { + scriptlet.content = this.base64ToUtf8_(scriptlet.content) + } + return scriptlets + }) + .catch((error) => { + throw error + }) + } + + addCustomScriptlet(scriptlet) { + scriptlet.content = this.utf8ToBase64_(scriptlet.content) + return sendWithPromise('brave_adblock.addCustomScriptlet', scriptlet) + } + + updateCustomScriptlet(name, scriptlet) { + scriptlet.content = this.utf8ToBase64_(scriptlet.content) + return sendWithPromise( + 'brave_adblock.updateCustomScriptlet', + name, + scriptlet + ) + } + + removeCustomScriptlet(name) { + return sendWithPromise('brave_adblock.removeCustomScriptlet', name) + } + + addWebUiListener(event_name, callback) { addWebUiListener(event_name, callback) } } diff --git a/browser/resources/settings/default_brave_shields_page/brave_adblock_subpage.html b/browser/resources/settings/default_brave_shields_page/brave_adblock_subpage.html index 254da6e9c217..2c032edecec4 100644 --- a/browser/resources/settings/default_brave_shields_page/brave_adblock_subpage.html +++ b/browser/resources/settings/default_brave_shields_page/brave_adblock_subpage.html @@ -328,3 +328,13 @@ > + + + + + $i18n{adblockCustomSciptletsListLabel} + + + + + diff --git a/browser/resources/settings/default_brave_shields_page/brave_adblock_subpage.ts b/browser/resources/settings/default_brave_shields_page/brave_adblock_subpage.ts index 5660ae8d40d1..773527887f50 100644 --- a/browser/resources/settings/default_brave_shields_page/brave_adblock_subpage.ts +++ b/browser/resources/settings/default_brave_shields_page/brave_adblock_subpage.ts @@ -19,6 +19,8 @@ import {BaseMixin} from '../base_mixin.js'; import {BraveAdblockBrowserProxyImpl} from './brave_adblock_browser_proxy.js' import {getTemplate} from './brave_adblock_subpage.html.js' +import { loadTimeData } from '../i18n_setup.js' + const AdBlockSubpageBase = PrefsMixin(I18nMixin(BaseMixin(PolymerElement))) class AdBlockSubpage extends AdBlockSubpageBase { @@ -41,6 +43,12 @@ class AdBlockSubpage extends AdBlockSubpageBase { type: Boolean, value: false }, + cosmeticFilteringCustomScriptletsEnabled_: { + type: Boolean, + value: loadTimeData.getBoolean( + 'cosmeticFilteringCustomScriptletsEnabled' + ) + } } } diff --git a/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_editor.html b/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_editor.html new file mode 100644 index 000000000000..f50c568fc443 --- /dev/null +++ b/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_editor.html @@ -0,0 +1,51 @@ + + + + [[dialogTitle_]] + + + $i18n{adblockCustomSciptletDialogNameLabel} + + + + + $i18n{adblockCustomScriptletDialogContentLabel} + + + + + + $i18n{adblockCustomScriptletDialogCancelButton} + + + $i18n{adblockCustomScriptletDialogSaveButton} + + + \ No newline at end of file diff --git a/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_editor.ts b/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_editor.ts new file mode 100644 index 000000000000..499155f5cd9c --- /dev/null +++ b/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_editor.ts @@ -0,0 +1,126 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import 'chrome://resources/cr_elements/cr_button/cr_button.js' +import { CrDialogElement } from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js' +import 'chrome://resources/cr_elements/cr_input/cr_input.js' + +import { PrefsMixin } from '/shared/settings/prefs/prefs_mixin.js' +import { I18nMixin } from 'chrome://resources/cr_elements/i18n_mixin.js' +import { PolymerElement } from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js' + +import { getTemplate } from './brave_adblock_scriptlet_editor.html.js' + +import { + Scriptlet, + BraveAdblockBrowserProxyImpl, + ErrorCode +} from '../brave_adblock_browser_proxy.js' + +interface AdblockScriptletEditor { + $: { + dialog: CrDialogElement + } +} + +const AdblockScriptletEditorBase = I18nMixin(PrefsMixin(PolymerElement)) + +class AdblockScriptletEditor extends AdblockScriptletEditorBase { + static get is() { + return 'adblock-scriptlet-editor' + } + + static get template() { + return getTemplate() + } + + static get properties() { + return { + scriptlet: Scriptlet, + dialogTitle_: String, + isScriptletValid_: Boolean, + scriptletErrorMessage_: String + } + } + + scriptlet: Scriptlet + dialogTitle_: string + isScriptletValid_: boolean + scriptletErrorMessage_: string + + scriptletName_: string + browserProxy_ = BraveAdblockBrowserProxyImpl.getInstance() + + override ready() { + super.ready() + this.scriptletName_ = this.scriptlet.name + + if (this.scriptletName_) { + this.dialogTitle_ = this.i18n('adblockEditCustomScriptletDialogTitle') + } else { + this.dialogTitle_ = this.i18n('adblockAddCustomScriptletDialogTitle') + } + + this.updateError(ErrorCode.kOK) + } + + updateError(error_code: ErrorCode) { + this.isScriptletValid_ = error_code === ErrorCode.kOK + switch (error_code) { + case ErrorCode.kOK: + this.scriptletErrorMessage_ = '' + break + case ErrorCode.kAlreadyExists: + this.scriptletErrorMessage_ = this.i18n( + 'adblockEditCustomScriptletAlreadyExistsError' + ) + break + case ErrorCode.kInvalidName: + this.scriptletErrorMessage_ = this.i18n( + 'adblockEditCustomScriptletInvalidNameError' + ) + break + case ErrorCode.kNotFound: + this.scriptletErrorMessage_ = this.i18n( + 'adblockEditCustomScriptletNotFoundError' + ) + break + } + } + + cancelClicked_() { + this.$.dialog.cancel() + } + + saveClicked_() { + if (this.scriptletName_) { + this.browserProxy_ + .updateCustomScriptlet(this.scriptletName_, this.scriptlet) + .then((e) => { + this.updateError(e) + if (this.isScriptletValid_) { + this.$.dialog.close() + } + }) + } else { + this.browserProxy_.addCustomScriptlet(this.scriptlet).then((e) => { + this.updateError(e) + if (this.isScriptletValid_) { + this.$.dialog.close() + } + }) + } + } + + validateName_() { + if (!/^[a-z-_.]*$/.test(this.scriptlet.name)) { + this.updateError(ErrorCode.kInvalidName) + } else { + this.updateError(ErrorCode.kOK) + } + } +} + +customElements.define(AdblockScriptletEditor.is, AdblockScriptletEditor) diff --git a/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_list.html b/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_list.html new file mode 100644 index 000000000000..e57ea6c5e2f3 --- /dev/null +++ b/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_list.html @@ -0,0 +1,51 @@ + + + + + + + + [[item.name]] + + + + + + + + + + + + + $i18n{adblockAddCustomScriptletButton} + + + + + + + \ No newline at end of file diff --git a/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_list.ts b/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_list.ts new file mode 100644 index 000000000000..4dd273057e82 --- /dev/null +++ b/browser/resources/settings/default_brave_shields_page/components/brave_adblock_scriptlet_list.ts @@ -0,0 +1,94 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import 'chrome://resources/cr_elements/cr_button/cr_button.js' +import 'chrome://resources/cr_elements/icons.html.js' + +import { PrefsMixin } from '/shared/settings/prefs/prefs_mixin.js' +import { I18nMixin } from 'chrome://resources/cr_elements/i18n_mixin.js' +import { PolymerElement } from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js' + +import { BaseMixin } from '../../base_mixin.js' + +import { getTemplate } from './brave_adblock_scriptlet_list.html.js' + +import { + Scriptlet, + BraveAdblockBrowserProxyImpl +} from '../brave_adblock_browser_proxy.js' + +import './brave_adblock_scriptlet_editor.js' + +const AdblockScriptletListBase = PrefsMixin( + I18nMixin(BaseMixin(PolymerElement)) +) + +class AdblockScriptletList extends AdblockScriptletListBase { + static get is() { + return 'adblock-scriptlet-list' + } + + static get template() { + return getTemplate() + } + + static get properties() { + return { + customScriptletsList_: { + type: Array + }, + editingScriptlet_: Scriptlet, + isEditing_: Boolean + } + } + + customScriptletsList_: Scriptlet[] + editingScriptlet_: Scriptlet | null = null + isEditing_: boolean = false + + browserProxy_ = BraveAdblockBrowserProxyImpl.getInstance() + + override ready() { + super.ready() + this.isEditing_ = false + this.browserProxy_.getCustomScriptlets().then((scriptlets) => { + this.customScriptletsList_ = scriptlets + }) + } + + handleAdd_(_: any) { + this.editingScriptlet_ = new Scriptlet() + this.isEditing_ = true + } + + handleEdit_(e: any) { + this.editingScriptlet_ = this.customScriptletsList_[e.model.index] + this.isEditing_ = true + } + + handleDelete_(e: any) { + const messageText = this.i18n('adblockCustomScriptletDeleteConfirmation') + if (!confirm(messageText)) { + return + } + + this.browserProxy_.removeCustomScriptlet( + this.customScriptletsList_[e.model.index].name + ) + this.browserProxy_.getCustomScriptlets().then((scriptlets) => { + this.customScriptletsList_ = scriptlets + }) + } + + scriptletEditorClosed_(_: any) { + this.editingScriptlet_ = null + this.isEditing_ = false + this.browserProxy_.getCustomScriptlets().then((scriptlets) => { + this.customScriptletsList_ = scriptlets + }) + } +} + +customElements.define(AdblockScriptletList.is, AdblockScriptletList) diff --git a/browser/resources/settings/sources.gni b/browser/resources/settings/sources.gni index 12280ddde9ce..83ae8f0d7f14 100644 --- a/browser/resources/settings/sources.gni +++ b/browser/resources/settings/sources.gni @@ -50,6 +50,8 @@ brave_settings_web_component_files = [ "brave_web3_domains_page/brave_web3_domains_page.ts", "default_brave_shields_page/brave_adblock_subpage.ts", "default_brave_shields_page/components/brave_adblock_editor.ts", + "default_brave_shields_page/components/brave_adblock_scriptlet_editor.ts", + "default_brave_shields_page/components/brave_adblock_scriptlet_list.ts", "default_brave_shields_page/components/brave_adblock_subscribe_dropdown.ts", "default_brave_shields_page/default_brave_shields_page.ts", "getting_started_page/getting_started.ts", diff --git a/browser/ui/webui/settings/brave_adblock_handler.cc b/browser/ui/webui/settings/brave_adblock_handler.cc index c2fc7523178a..95c1ccef1dd7 100644 --- a/browser/ui/webui/settings/brave_adblock_handler.cc +++ b/browser/ui/webui/settings/brave_adblock_handler.cc @@ -9,6 +9,7 @@ #include #include +#include "base/feature_list.h" #include "base/functional/bind.h" #include "base/json/values_util.h" #include "base/values.h" @@ -17,6 +18,8 @@ #include "brave/components/brave_shields/content/browser/ad_block_custom_filters_provider.h" #include "brave/components/brave_shields/content/browser/ad_block_service.h" #include "brave/components/brave_shields/core/browser/ad_block_component_service_manager.h" +#include "brave/components/brave_shields/core/browser/ad_block_custom_resource_provider.h" +#include "brave/components/brave_shields/core/common/features.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/content_settings/host_content_settings_map_factory.h" #include "chrome/browser/profiles/profile.h" @@ -86,6 +89,26 @@ void BraveAdBlockHandler::RegisterMessages() { "brave_adblock.updateCustomFilters", base::BindRepeating(&BraveAdBlockHandler::UpdateCustomFilters, base::Unretained(this))); + + web_ui()->RegisterMessageCallback( + "brave_adblock.getCustomScriptlets", + base::BindRepeating(&BraveAdBlockHandler::GetCustomScriptlets, + base::Unretained(this))); + + web_ui()->RegisterMessageCallback( + "brave_adblock.addCustomScriptlet", + base::BindRepeating(&BraveAdBlockHandler::AddCustomScriptlet, + base::Unretained(this))); + + web_ui()->RegisterMessageCallback( + "brave_adblock.updateCustomScriptlet", + base::BindRepeating(&BraveAdBlockHandler::UpdateCustomScriptlet, + base::Unretained(this))); + + web_ui()->RegisterMessageCallback( + "brave_adblock.removeCustomScriptlet", + base::BindRepeating(&BraveAdBlockHandler::RemoveCustomScriptlet, + base::Unretained(this))); } void BraveAdBlockHandler::OnJavascriptAllowed() { @@ -117,8 +140,9 @@ void BraveAdBlockHandler::GetRegionalLists(const base::Value::List& args) { void BraveAdBlockHandler::EnableFilterList(const base::Value::List& args) { DCHECK_EQ(args.size(), 2U); - if (!args[0].is_string() || !args[1].is_bool()) + if (!args[0].is_string() || !args[1].is_bool()) { return; + } std::string uuid = args[0].GetString(); bool enabled = args[1].GetBool(); @@ -162,14 +186,16 @@ void BraveAdBlockHandler::GetCustomFilters(const base::Value::List& args) { void BraveAdBlockHandler::AddSubscription(const base::Value::List& args) { DCHECK_EQ(args.size(), 1U); AllowJavascript(); - if (!args[0].is_string()) + if (!args[0].is_string()) { return; + } std::string subscription_url_string = args[0].GetString(); const GURL subscription_url = GURL(subscription_url_string); - if (!subscription_url.is_valid()) + if (!subscription_url.is_valid()) { return; + } g_brave_browser_process->ad_block_service() ->subscription_service_manager() @@ -182,14 +208,16 @@ void BraveAdBlockHandler::SetSubscriptionEnabled( const base::Value::List& args) { DCHECK_EQ(args.size(), 2U); AllowJavascript(); - if (!args[0].is_string() || !args[1].is_bool()) + if (!args[0].is_string() || !args[1].is_bool()) { return; + } std::string subscription_url_string = args[0].GetString(); bool enabled = args[1].GetBool(); const GURL subscription_url = GURL(subscription_url_string); - if (!subscription_url.is_valid()) + if (!subscription_url.is_valid()) { return; + } g_brave_browser_process->ad_block_service() ->subscription_service_manager() ->EnableSubscription(subscription_url, enabled); @@ -200,8 +228,9 @@ void BraveAdBlockHandler::SetSubscriptionEnabled( void BraveAdBlockHandler::UpdateSubscription(const base::Value::List& args) { DCHECK_EQ(args.size(), 1U); AllowJavascript(); - if (!args[0].is_string()) + if (!args[0].is_string()) { return; + } std::string subscription_url_string = args[0].GetString(); const GURL subscription_url = GURL(subscription_url_string); @@ -217,8 +246,9 @@ void BraveAdBlockHandler::UpdateSubscription(const base::Value::List& args) { void BraveAdBlockHandler::DeleteSubscription(const base::Value::List& args) { DCHECK_EQ(args.size(), 1U); AllowJavascript(); - if (!args[0].is_string()) + if (!args[0].is_string()) { return; + } std::string subscription_url_string = args[0].GetString(); const GURL subscription_url = GURL(subscription_url_string); @@ -235,8 +265,9 @@ void BraveAdBlockHandler::DeleteSubscription(const base::Value::List& args) { void BraveAdBlockHandler::ViewSubscriptionSource( const base::Value::List& args) { DCHECK_EQ(args.size(), 1U); - if (!args[0].is_string()) + if (!args[0].is_string()) { return; + } std::string subscription_url_string = args[0].GetString(); const GURL subscription_url = GURL(subscription_url_string); @@ -253,8 +284,9 @@ void BraveAdBlockHandler::ViewSubscriptionSource( } void BraveAdBlockHandler::UpdateCustomFilters(const base::Value::List& args) { - if (!args[0].is_string()) + if (!args[0].is_string()) { return; + } std::string custom_filters = args[0].GetString(); g_brave_browser_process->ad_block_service() @@ -262,6 +294,58 @@ void BraveAdBlockHandler::UpdateCustomFilters(const base::Value::List& args) { ->UpdateCustomFilters(custom_filters); } +void BraveAdBlockHandler::GetCustomScriptlets(const base::Value::List& args) { + CHECK(base::FeatureList::IsEnabled( + brave_shields::features::kCosmeticFilteringCustomScriptlets)); + CHECK(args.size() == 1u && args[0].is_string()); + AllowJavascript(); + + const auto& custom_resources = g_brave_browser_process->ad_block_service() + ->custom_resource_provider() + ->GetCustomResources(); + + ResolveJavascriptCallback(args[0].GetString(), custom_resources); +} + +void BraveAdBlockHandler::AddCustomScriptlet(const base::Value::List& args) { + CHECK(base::FeatureList::IsEnabled( + brave_shields::features::kCosmeticFilteringCustomScriptlets)); + CHECK(args.size() == 2u && args[0].is_string() && args[1].is_dict()); + AllowJavascript(); + auto error_code = g_brave_browser_process->ad_block_service() + ->custom_resource_provider() + ->AddResource(args[1]); + ResolveJavascriptCallback(args[0].GetString(), + base::Value(static_cast(error_code))); +} + +void BraveAdBlockHandler::UpdateCustomScriptlet(const base::Value::List& args) { + CHECK(base::FeatureList::IsEnabled( + brave_shields::features::kCosmeticFilteringCustomScriptlets)); + CHECK(args.size() == 3u && args[0].is_string() && args[1].is_string() && + args[2].is_dict()); + AllowJavascript(); + auto error_code = g_brave_browser_process->ad_block_service() + ->custom_resource_provider() + ->UpdateResource(args[1].GetString(), args[2]); + ResolveJavascriptCallback(args[0].GetString(), + base::Value(static_cast(error_code))); +} + +void BraveAdBlockHandler::RemoveCustomScriptlet(const base::Value::List& args) { + CHECK(base::FeatureList::IsEnabled( + brave_shields::features::kCosmeticFilteringCustomScriptlets)); + AllowJavascript(); + CHECK(args.size() == 2u && args[0].is_string() && args[1].is_string()); + AllowJavascript(); + auto error_code = g_brave_browser_process->ad_block_service() + ->custom_resource_provider() + ->RemoveResource(args[1].GetString()); + + ResolveJavascriptCallback(args[0].GetString(), + base::Value(static_cast(error_code))); +} + void BraveAdBlockHandler::RefreshSubscriptionsList() { FireWebUIListener("brave_adblock.onGetListSubscriptions", GetSubscriptions()); } diff --git a/browser/ui/webui/settings/brave_adblock_handler.h b/browser/ui/webui/settings/brave_adblock_handler.h index d4c84ec4deb2..cff4a0dd37f0 100644 --- a/browser/ui/webui/settings/brave_adblock_handler.h +++ b/browser/ui/webui/settings/brave_adblock_handler.h @@ -49,6 +49,10 @@ class BraveAdBlockHandler : public settings::SettingsPageUIHandler, void DeleteSubscription(const base::Value::List& args); void ViewSubscriptionSource(const base::Value::List& args); void UpdateCustomFilters(const base::Value::List& args); + void GetCustomScriptlets(const base::Value::List& args); + void AddCustomScriptlet(const base::Value::List& args); + void UpdateCustomScriptlet(const base::Value::List& args); + void RemoveCustomScriptlet(const base::Value::List& args); void RefreshSubscriptionsList(); diff --git a/browser/ui/webui/settings/brave_settings_localized_strings_provider.cc b/browser/ui/webui/settings/brave_settings_localized_strings_provider.cc index 459eceb1b801..6a46c3efba77 100644 --- a/browser/ui/webui/settings/brave_settings_localized_strings_provider.cc +++ b/browser/ui/webui/settings/brave_settings_localized_strings_provider.cc @@ -721,6 +721,30 @@ void BraveAddCommonStrings(content::WebUIDataSource* html_source, {"adblockSubscribeUrlUpdateFailed", IDS_BRAVE_ADBLOCK_SUBSCRIBE_URL_UPDATE_FAILED}, {"adblockCustomListsLabel", IDS_BRAVE_ADBLOCK_CUSTOM_LISTS_LABEL}, + {"adblockCustomSciptletsListLabel", + IDS_BRAVE_ADBLOCK_CUSTOM_SCRIPTLETS_LIST_LABEL}, + {"adblockAddCustomScriptletButton", + IDS_BRAVE_ADBLOCK_ADD_CUSTOM_SCRIPTLET_BUTTON}, + {"adblockAddCustomScriptletDialogTitle", + IDS_BRAVE_ADBLOCK_ADD_CUSTOM_SCRIPTLET_DIALOG_TITLE}, + {"adblockEditCustomScriptletDialogTitle", + IDS_BRAVE_ADBLOCK_EDIT_CUSTOM_SCRIPTLET_DIALOG_TITLE}, + {"adblockCustomSciptletDialogNameLabel", + IDS_BRAVE_ADBLOCK_CUSTOM_SCRIPTLET_DIALOG_NAME_LABEL}, + {"adblockCustomScriptletDialogContentLabel", + IDS_BRAVE_ADBLOCK_CUSTOM_SCRIPTLET_DIALOG_CONTENT_LABEL}, + {"adblockCustomScriptletDialogCancelButton", + IDS_BRAVE_ADBLOCK_CUSTOM_SCRIPTLET_DIALOG_CANCEL_BUTTON}, + {"adblockCustomScriptletDialogSaveButton", + IDS_BRAVE_ADBLOCK_CUSTOM_SCRIPTLET_DIALOG_SAVE_BUTTON}, + {"adblockCustomScriptletDeleteConfirmation", + IDS_BRAVE_ADBLOCK_CUSTOM_SCRIPTLET_DELETE_CONFIRMATION}, + {"adblockEditCustomScriptletAlreadyExistsError", + IDS_BRAVE_ADBLOCK_CUSTOM_SCRIPTLET_ALREADY_EXISTS_ERROR}, + {"adblockEditCustomScriptletInvalidNameError", + IDS_BRAVE_ADBLOCK_CUSTOM_SCRIPTLET_INVALID_NAME_ERROR}, + {"adblockEditCustomScriptletNotFoundError", + IDS_BRAVE_ADBLOCK_CUSTOM_SCRIPTLET_NOT_FOUND_ERROR}, {"braveShortcutsPage", IDS_SETTINGS_BRAVE_SHORTCUTS_TITLE}, {"shortcutsPageSearchPlaceholder", IDS_SHORTCUTS_PAGE_SEARCH_PLACEHOLDER}, @@ -852,6 +876,11 @@ void BraveAddLocalizedStrings(content::WebUIDataSource* html_source, html_source->AddLocalizedStrings(kSessionOnlyToEphemeralStrings); } + html_source->AddBoolean( + "cosmeticFilteringCustomScriptletsEnabled", + base::FeatureList::IsEnabled( + brave_shields::features::kCosmeticFilteringCustomScriptlets)); + // Always disable upstream's side panel align option. // We add our customized option at preferred position. html_source->AddBoolean("showSidePanelOptions", false); diff --git a/components/brave_shields/content/browser/ad_block_service.cc b/components/brave_shields/content/browser/ad_block_service.cc index a823cf99b892..c90c1203c1c8 100644 --- a/components/brave_shields/content/browser/ad_block_service.cc +++ b/components/brave_shields/content/browser/ad_block_service.cc @@ -22,6 +22,7 @@ #include "brave/components/brave_shields/content/browser/ad_block_subscription_service_manager.h" #include "brave/components/brave_shields/core/browser/ad_block_component_filters_provider.h" #include "brave/components/brave_shields/core/browser/ad_block_component_service_manager.h" +#include "brave/components/brave_shields/core/browser/ad_block_custom_resource_provider.h" #include "brave/components/brave_shields/core/browser/ad_block_default_resource_provider.h" #include "brave/components/brave_shields/core/browser/ad_block_filter_list_catalog_provider.h" #include "brave/components/brave_shields/core/browser/ad_block_filters_provider_manager.h" @@ -264,6 +265,11 @@ AdBlockCustomFiltersProvider* AdBlockService::custom_filters_provider() { return custom_filters_provider_.get(); } +AdBlockCustomResourceProvider* AdBlockService::custom_resource_provider() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return custom_resource_provider_.get(); +} + AdBlockSubscriptionServiceManager* AdBlockService::subscription_service_manager() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); @@ -306,8 +312,19 @@ AdBlockService::AdBlockService( SetupDiscardPolicy(policy); } - resource_provider_ = std::make_unique( - component_update_service_); + auto default_resource_provider = + std::make_unique( + component_update_service_); + default_resource_provider_ = default_resource_provider.get(); + + if (base::FeatureList::IsEnabled( + features::kCosmeticFilteringCustomScriptlets)) { + custom_resource_provider_ = new AdBlockCustomResourceProvider( + local_state_, std::move(default_resource_provider)); + resource_provider_.reset(custom_resource_provider_.get()); + } else { + resource_provider_ = std::move(default_resource_provider); + } filter_list_catalog_provider_ = std::make_unique( component_update_service_); @@ -401,15 +418,16 @@ void RegisterPrefsForAdBlockService(PrefRegistrySimple* registry) { registry->RegisterBooleanPref( prefs::kAdBlockMobileNotificationsListSettingTouched, false); registry->RegisterStringPref(prefs::kAdBlockCustomFilters, std::string()); + registry->RegisterListPref(prefs::kAdBlockCustomResources); registry->RegisterDictionaryPref(prefs::kAdBlockRegionalFilters); registry->RegisterDictionaryPref(prefs::kAdBlockListSubscriptions); registry->RegisterBooleanPref(prefs::kAdBlockCheckedDefaultRegion, false); registry->RegisterBooleanPref(prefs::kAdBlockCheckedAllDefaultRegions, false); } -AdBlockResourceProvider* AdBlockService::resource_provider() { +AdBlockDefaultResourceProvider* AdBlockService::default_resource_provider() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - return resource_provider_.get(); + return default_resource_provider_.get(); } void AdBlockService::UseSourceProviderForTest( diff --git a/components/brave_shields/content/browser/ad_block_service.h b/components/brave_shields/content/browser/ad_block_service.h index f7966d2011ff..a276d6e284da 100644 --- a/components/brave_shields/content/browser/ad_block_service.h +++ b/components/brave_shields/content/browser/ad_block_service.h @@ -47,6 +47,7 @@ class AdBlockComponentFiltersProvider; class AdBlockDefaultResourceProvider; class AdBlockComponentServiceManager; class AdBlockCustomFiltersProvider; +class AdBlockCustomResourceProvider; class AdBlockLocalhostFiltersProvider; class AdBlockFilterListCatalogProvider; class AdBlockSubscriptionServiceManager; @@ -82,8 +83,9 @@ class AdBlockService { std::unique_ptr> filter_set_; raw_ptr adblock_engine_; - raw_ptr filters_provider_; // not owned - raw_ptr resource_provider_; // not owned + raw_ptr filters_provider_; // not owned + raw_ptr resource_provider_; // not owned + raw_ptr custom_resource_provider_; // not owned scoped_refptr task_runner_; bool is_filter_provider_manager_; @@ -123,6 +125,7 @@ class AdBlockService { AdBlockComponentServiceManager* component_service_manager(); AdBlockSubscriptionServiceManager* subscription_service_manager(); AdBlockCustomFiltersProvider* custom_filters_provider(); + AdBlockCustomResourceProvider* custom_resource_provider(); void EnableTag(const std::string& tag, bool enabled); void AddUserCosmeticFilter(const std::string& filter); @@ -149,7 +152,7 @@ class AdBlockService { static std::string g_ad_block_dat_file_version_; - AdBlockResourceProvider* resource_provider(); + AdBlockDefaultResourceProvider* default_resource_provider(); AdBlockComponentFiltersProvider* default_filters_provider() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); return default_filters_provider_.get(); @@ -181,8 +184,12 @@ class AdBlockService { AdBlockListP3A list_p3a_; - std::unique_ptr resource_provider_ + std::unique_ptr resource_provider_ GUARDED_BY_CONTEXT(sequence_checker_); + raw_ptr default_resource_provider_ + GUARDED_BY_CONTEXT(sequence_checker_) = nullptr; + raw_ptr custom_resource_provider_ + GUARDED_BY_CONTEXT(sequence_checker_) = nullptr; std::unique_ptr custom_filters_provider_ GUARDED_BY_CONTEXT(sequence_checker_); std::unique_ptr localhost_filters_provider_ diff --git a/components/brave_shields/core/browser/BUILD.gn b/components/brave_shields/core/browser/BUILD.gn index efde19effef2..b5049455c628 100644 --- a/components/brave_shields/core/browser/BUILD.gn +++ b/components/brave_shields/core/browser/BUILD.gn @@ -11,6 +11,8 @@ static_library("browser") { "ad_block_component_installer.h", "ad_block_component_service_manager.cc", "ad_block_component_service_manager.h", + "ad_block_custom_resource_provider.cc", + "ad_block_custom_resource_provider.h", "ad_block_default_resource_provider.cc", "ad_block_default_resource_provider.h", "ad_block_filter_list_catalog_provider.cc", diff --git a/components/brave_shields/core/browser/ad_block_custom_resource_provider.cc b/components/brave_shields/core/browser/ad_block_custom_resource_provider.cc new file mode 100644 index 000000000000..a43f0717ea46 --- /dev/null +++ b/components/brave_shields/core/browser/ad_block_custom_resource_provider.cc @@ -0,0 +1,209 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/brave_shields/core/browser/ad_block_custom_resource_provider.h" + +#include +#include + +#include "base/feature_list.h" +#include "base/json/json_writer.h" +#include "base/ranges/algorithm.h" +#include "base/strings/strcat.h" +#include "base/strings/string_util.h" +#include "brave/components/brave_shields/core/common/features.h" +#include "brave/components/brave_shields/core/common/pref_names.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/scoped_user_pref_update.h" + +namespace brave_shields { + +namespace { + +constexpr const char kNameField[] = "name"; +constexpr const char kContentField[] = "content"; +constexpr const char kKindField[] = "kind"; +constexpr const char kMimeField[] = "mime"; + +bool IsValidResource(const base::Value& resource) { + if (!resource.is_dict() || !resource.GetDict().FindString(kNameField) || + !resource.GetDict().FindString(kContentField)) { + // Invalid resource structure. + return false; + } + + const auto* name = resource.GetDict().FindString(kNameField); + if (name->empty() || !base::IsStringASCII(*name)) { + return false; + } + + auto* kind = resource.GetDict().FindDict(kKindField); + if (!kind || !kind->FindString(kMimeField)) { + return false; + } + return true; +} + +const std::string& GetResourceName(const base::Value& resource) { + DCHECK(IsValidResource(resource)); + return *resource.GetDict().FindString(kNameField); +} + +base::Value::List::iterator FindResource(base::Value::List& resources, + const std::string& name) { + return base::ranges::find_if(resources, [name](const base::Value& v) { + CHECK(IsValidResource(v)); + return GetResourceName(v) == name; + }); +} + +std::string_view JsonListStr(std::string_view json) { + const auto start = json.find('['); + const auto end = json.rfind(']'); + if (start == std::string_view::npos || end == std::string_view::npos || + start >= end) { + return std::string_view(); + } + return json.substr(start + 1, end - start - 1); +} + +std::string MergeResources(const std::string& default_resources, + const std::string& custom_resources) { + auto default_resources_str = JsonListStr(default_resources); + if (default_resources_str.empty()) { + return custom_resources; + } + auto custom_resources_str = JsonListStr(custom_resources); + if (custom_resources_str.empty()) { + return default_resources; + } + return base::StrCat( + {"[", default_resources_str, ",", custom_resources_str, "]"}); +} + +base::Value FixName(const base::Value& resource) { + base::Value result = resource.Clone(); + + std::string* name = result.GetDict().FindString(kNameField); + *name = base::ToLowerASCII(*name); + if (!base::EndsWith(*name, ".js")) { + name->append(".js"); + } + + return result; +} + +} // namespace + +AdBlockCustomResourceProvider::AdBlockCustomResourceProvider( + PrefService* local_state, + std::unique_ptr default_resource_provider) + : local_state_(local_state), + default_resource_provider_(std::move(default_resource_provider)) { + CHECK(base::FeatureList::IsEnabled( + brave_shields::features::kCosmeticFilteringCustomScriptlets)); + CHECK(default_resource_provider_); + default_resource_provider_->AddObserver(this); +} + +AdBlockCustomResourceProvider::~AdBlockCustomResourceProvider() { + default_resource_provider_->RemoveObserver(this); +} + +const base::Value& AdBlockCustomResourceProvider::GetCustomResources() { + return local_state_->GetValue(prefs::kAdBlockCustomResources); +} + +AdBlockCustomResourceProvider::ErrorCode +AdBlockCustomResourceProvider::AddResource(const base::Value& resource) { + if (!IsValidResource(resource)) { + return ErrorCode::kInvalid; + } + + ScopedListPrefUpdate update(local_state_, prefs::kAdBlockCustomResources); + if (FindResource(update.Get(), GetResourceName(resource)) != update->end()) { + return ErrorCode::kAlreadyExists; + } + update->Append(FixName(resource)); + ReloadResourcesAndNotify(); + return ErrorCode::kOk; +} + +AdBlockCustomResourceProvider::ErrorCode +AdBlockCustomResourceProvider::UpdateResource(const std::string& old_name, + const base::Value& resource) { + if (!IsValidResource(resource)) { + return ErrorCode::kInvalid; + } + ScopedListPrefUpdate update(local_state_, prefs::kAdBlockCustomResources); + auto updated_resource = FindResource(update.Get(), old_name); + if (updated_resource == update->end()) { + return ErrorCode::kNotFound; + } + const std::string& new_name = GetResourceName(resource); + if (old_name != new_name) { + if (FindResource(update.Get(), new_name) != update->end()) { + return ErrorCode::kNotFound; + } + } + + *updated_resource = FixName(resource); + ReloadResourcesAndNotify(); + return ErrorCode::kOk; +} + +AdBlockCustomResourceProvider::ErrorCode +AdBlockCustomResourceProvider::RemoveResource( + const std::string& resource_name) { + ScopedListPrefUpdate update(local_state_, prefs::kAdBlockCustomResources); + auto updated_resource = FindResource(update.Get(), resource_name); + update->erase(updated_resource); + ReloadResourcesAndNotify(); + return ErrorCode::kOk; +} + +void AdBlockCustomResourceProvider::LoadResources( + base::OnceCallback on_load) { + default_resource_provider_->LoadResources( + base::BindOnce(&AdBlockCustomResourceProvider::OnDefaultResourcesLoaded, + weak_ptr_factory_.GetWeakPtr(), std::move(on_load))); +} + +void AdBlockCustomResourceProvider::OnResourcesLoaded( + const std::string& resources_json) { + NotifyResourcesLoaded( + MergeResources(resources_json, GetCustomResourcesJson())); +} + +std::string AdBlockCustomResourceProvider::GetCustomResourcesJson() { + return base::WriteJson(GetCustomResources()).value_or(std::string()); +} + +void AdBlockCustomResourceProvider::OnDefaultResourcesLoaded( + base::OnceCallback on_load, + const std::string& resources_json) { + const auto& custom_resources = + local_state_->GetList(prefs::kAdBlockCustomResources); + std::string custom_resources_json = + base::WriteJson(custom_resources).value_or(std::string()); + + if (custom_resources.empty()) { + std::move(on_load).Run(resources_json); + } else if (resources_json.empty()) { + std::move(on_load).Run(custom_resources_json); + } else { + // Merge. + std::move(on_load).Run( + MergeResources(resources_json, custom_resources_json)); + } +} + +void AdBlockCustomResourceProvider::ReloadResourcesAndNotify() { + LoadResources( + base::BindOnce(&AdBlockCustomResourceProvider::NotifyResourcesLoaded, + weak_ptr_factory_.GetWeakPtr())); +} + +} // namespace brave_shields diff --git a/components/brave_shields/core/browser/ad_block_custom_resource_provider.h b/components/brave_shields/core/browser/ad_block_custom_resource_provider.h new file mode 100644 index 000000000000..5f5a2fae0097 --- /dev/null +++ b/components/brave_shields/core/browser/ad_block_custom_resource_provider.h @@ -0,0 +1,64 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_BRAVE_SHIELDS_CORE_BROWSER_AD_BLOCK_CUSTOM_RESOURCE_PROVIDER_H_ +#define BRAVE_COMPONENTS_BRAVE_SHIELDS_CORE_BROWSER_AD_BLOCK_CUSTOM_RESOURCE_PROVIDER_H_ + +#include + +#include "base/functional/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/values.h" +#include "brave/components/brave_shields/core/browser/ad_block_resource_provider.h" + +class PrefService; + +namespace brave_shields { + +class AdBlockCustomResourceProvider + : public AdBlockResourceProvider, + private AdBlockResourceProvider::Observer { + public: + enum class ErrorCode { + kOk, + kInvalid, + kAlreadyExists, + kNotFound, + }; + + AdBlockCustomResourceProvider( + PrefService* local_state, + std::unique_ptr default_resource_provider); + ~AdBlockCustomResourceProvider() override; + + const base::Value& GetCustomResources(); + ErrorCode AddResource(const base::Value& resource); + ErrorCode UpdateResource(const std::string& name, + const base::Value& resource); + ErrorCode RemoveResource(const std::string& resource_name); + + // AdBlockResourceProvider: + void LoadResources( + base::OnceCallback) override; + + private: + // AdBlockResourceProvider::Observer: + void OnResourcesLoaded(const std::string& resources_json) override; + + std::string GetCustomResourcesJson(); + void OnDefaultResourcesLoaded( + base::OnceCallback on_load, + const std::string& resources_json); + void ReloadResourcesAndNotify(); + + const raw_ptr local_state_ = nullptr; + std::unique_ptr default_resource_provider_ = nullptr; + + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace brave_shields + +#endif // BRAVE_COMPONENTS_BRAVE_SHIELDS_CORE_BROWSER_AD_BLOCK_CUSTOM_RESOURCE_PROVIDER_H_ diff --git a/components/brave_shields/core/browser/ad_block_default_resource_provider.cc b/components/brave_shields/core/browser/ad_block_default_resource_provider.cc index a48d48497ad0..e2e649c4418e 100644 --- a/components/brave_shields/core/browser/ad_block_default_resource_provider.cc +++ b/components/brave_shields/core/browser/ad_block_default_resource_provider.cc @@ -10,6 +10,7 @@ #include "base/files/file_path.h" #include "base/task/thread_pool.h" +#include "brave/components/brave_component_updater/browser/dat_file_util.h" #include "brave/components/brave_shields/core/browser/ad_block_component_installer.h" namespace { @@ -59,7 +60,7 @@ void AdBlockDefaultResourceProvider::OnComponentReady( FROM_HERE, {base::MayBlock()}, base::BindOnce(&brave_component_updater::GetDATFileAsString, resources_path), - base::BindOnce(&AdBlockDefaultResourceProvider::OnResourcesLoaded, + base::BindOnce(&AdBlockDefaultResourceProvider::NotifyResourcesLoaded, weak_factory_.GetWeakPtr())); } diff --git a/components/brave_shields/core/browser/ad_block_resource_provider.cc b/components/brave_shields/core/browser/ad_block_resource_provider.cc index 0f08aaee3510..7c590a5c6812 100644 --- a/components/brave_shields/core/browser/ad_block_resource_provider.cc +++ b/components/brave_shields/core/browser/ad_block_resource_provider.cc @@ -27,7 +27,7 @@ void AdBlockResourceProvider::RemoveObserver( } } -void AdBlockResourceProvider::OnResourcesLoaded( +void AdBlockResourceProvider::NotifyResourcesLoaded( const std::string& resources_json) { for (auto& observer : observers_) { observer.OnResourcesLoaded(resources_json); diff --git a/components/brave_shields/core/browser/ad_block_resource_provider.h b/components/brave_shields/core/browser/ad_block_resource_provider.h index c478ad19d238..0d5d9550ece5 100644 --- a/components/brave_shields/core/browser/ad_block_resource_provider.h +++ b/components/brave_shields/core/browser/ad_block_resource_provider.h @@ -36,7 +36,7 @@ class AdBlockResourceProvider { base::OnceCallback) = 0; protected: - void OnResourcesLoaded(const std::string& resources_json); + void NotifyResourcesLoaded(const std::string& resources_json); private: base::ObserverList observers_; diff --git a/components/brave_shields/core/common/features.cc b/components/brave_shields/core/common/features.cc index 7c8089f19da2..8608f364bef7 100644 --- a/components/brave_shields/core/common/features.cc +++ b/components/brave_shields/core/common/features.cc @@ -121,6 +121,10 @@ BASE_FEATURE(kCosmeticFilteringJsPerformance, "CosmeticFilteringJsPerformance", base::FEATURE_ENABLED_BY_DEFAULT); +BASE_FEATURE(kCosmeticFilteringCustomScriptlets, + "CostemicFilteringCustomScriptlets", + base::FEATURE_DISABLED_BY_DEFAULT); + constexpr base::FeatureParam kComponentUpdateCheckIntervalMins{ &kAdBlockDefaultResourceUpdateInterval, "update_interval_mins", 100}; diff --git a/components/brave_shields/core/common/features.h b/components/brave_shields/core/common/features.h index dca25c5a1f70..b5d84476a51a 100644 --- a/components/brave_shields/core/common/features.h +++ b/components/brave_shields/core/common/features.h @@ -36,6 +36,7 @@ BASE_DECLARE_FEATURE(kBraveShowStrictFingerprintingMode); BASE_DECLARE_FEATURE(kCosmeticFilteringExtraPerfMetrics); BASE_DECLARE_FEATURE(kCosmeticFilteringJsPerformance); BASE_DECLARE_FEATURE(kCosmeticFilteringSyncLoad); +BASE_DECLARE_FEATURE(kCosmeticFilteringCustomScriptlets); extern const base::FeatureParam kComponentUpdateCheckIntervalMins; extern const base::FeatureParam kCosmeticFilteringSubFrameFirstSelectorsPollingDelayMs; diff --git a/components/brave_shields/core/common/pref_names.h b/components/brave_shields/core/common/pref_names.h index bbe3a4a999a3..1613b8ffe718 100644 --- a/components/brave_shields/core/common/pref_names.h +++ b/components/brave_shields/core/common/pref_names.h @@ -21,6 +21,8 @@ inline constexpr char kAdBlockMobileNotificationsListSettingTouched[] = "brave.ad_block.mobile_notifications_list_setting_touched"; inline constexpr char kAdBlockCustomFilters[] = "brave.ad_block.custom_filters"; +inline constexpr char kAdBlockCustomResources[] = + "brave.ad_block.custom_resources"; inline constexpr char kAdBlockRegionalFilters[] = "brave.ad_block.regional_filters"; inline constexpr char kAdBlockListSubscriptions[] =