summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan-Michael Brummer <jan.brummer@tabos.org>2021-08-10 14:46:39 +0200
committerMichael Catanzaro <mcatanzaro@gnome.org>2021-08-12 19:25:01 +0000
commitabd396a7f8c70100f9604c77a3ff23905703cadc (patch)
tree83dc76f16475a4944fc7d3d5fbdd3d90bb563ed4
parentb2829e2b8a20f13d3348d5dd3af0d47e66cb1a0d (diff)
downloadepiphany-abd396a7f8c70100f9604c77a3ff23905703cadc.tar.gz
Add AdGuard youtube blocker
Minimal solution, add fixed AdGuard youtube blocker. Fixes: https://gitlab.gnome.org/GNOME/epiphany/-/issues/1534 Part-of: <https://gitlab.gnome.org/GNOME/epiphany/-/merge_requests/1000> (cherry picked from commit 070abe3bafa5249b69690d8d30d033a2dace88aa)
-rw-r--r--embed/ephy-web-view.c30
-rw-r--r--src/meson.build1
-rw-r--r--tests/meson.build2
-rw-r--r--third-party/adguard/README.epiphany.md10
-rw-r--r--third-party/adguard/adguard.gresource.xml6
-rw-r--r--third-party/adguard/youtube.js466
-rw-r--r--third-party/meson.build7
7 files changed, 522 insertions, 0 deletions
diff --git a/embed/ephy-web-view.c b/embed/ephy-web-view.c
index 2b1f1ee9f..ab0b57b5f 100644
--- a/embed/ephy-web-view.c
+++ b/embed/ephy-web-view.c
@@ -4148,6 +4148,32 @@ ephy_web_view_class_init (EphyWebViewClass *klass)
0);
}
+static void
+ephy_web_view_ucm_add_custom_scripts (WebKitUserContentManager *ucm)
+{
+ WebKitUserScript *script;
+ g_autoptr (GBytes) youtube_js = NULL;
+ g_auto (GStrv) allow_list = NULL;
+ g_autoptr (GError) error = NULL;
+
+ youtube_js = g_resources_lookup_data ("/org/gnome/epiphany/adguard/youtube.js", 0, &error);
+ if (!youtube_js) {
+ g_warning ("Failed to load youtube.js from AdGuard: %s", error->message);
+ return;
+ }
+
+ allow_list = g_new (char *, 2);
+ allow_list[0] = g_strdup ("https://*.youtube.com/*");
+ allow_list[1] = NULL;
+
+ script = webkit_user_script_new (g_bytes_get_data (youtube_js, NULL),
+ WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
+ WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END,
+ (const char * const *)allow_list,
+ NULL);
+ webkit_user_content_manager_add_script (ucm, script);
+}
+
/**
* ephy_web_view_new:
*
@@ -4165,6 +4191,8 @@ ephy_web_view_new (void)
ephy_embed_shell_register_ucm_handler (shell, ucm);
ephy_embed_prefs_register_ucm (ucm);
+ ephy_web_view_ucm_add_custom_scripts (ucm);
+
return g_object_new (EPHY_TYPE_WEB_VIEW,
"web-context", ephy_embed_shell_get_web_context (shell),
"user-content-manager", ucm,
@@ -4179,6 +4207,8 @@ ephy_web_view_new_with_related_view (WebKitWebView *related_view)
EphyEmbedShell *shell = ephy_embed_shell_get_default ();
WebKitUserContentManager *ucm = webkit_user_content_manager_new ();
+ ephy_web_view_ucm_add_custom_scripts (ucm);
+
ephy_embed_shell_register_ucm_handler (shell, ucm);
ephy_embed_prefs_register_ucm (ucm);
diff --git a/src/meson.build b/src/meson.build
index 7f77173f2..5bf3eb92e 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -146,6 +146,7 @@ epiphany_sources = [
pdfjs_resources,
highlightjs_resources,
readability_resources,
+ adguard_resources
]
epiphany = executable('epiphany',
diff --git a/tests/meson.build b/tests/meson.build
index aebe007c0..bfee4398c 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -33,6 +33,7 @@ if get_option('unit_tests').enabled()
embed_shell_test = executable('test-ephy-embed-shell',
'ephy-embed-shell-test.c',
+ adguard_resources,
dependencies: ephymain_dep,
c_args: test_cargs + ['-DTEST_DIR="' + meson.current_source_dir() + '"'],
)
@@ -172,6 +173,7 @@ if get_option('unit_tests').enabled()
'ephy-web-view-test.c',
resources,
readability_resources,
+ adguard_resources,
dependencies: ephymain_dep
)
test('Web view test',
diff --git a/third-party/adguard/README.epiphany.md b/third-party/adguard/README.epiphany.md
new file mode 100644
index 000000000..aaeba749e
--- /dev/null
+++ b/third-party/adguard/README.epiphany.md
@@ -0,0 +1,10 @@
+# AdGuard
+
+This directory contains an official BlockYouTubeAdsShortcut version, distributed at: https://github.com/AdguardTeam/BlockYouTubeAdsShortcut/
+
+## Update process
+1. Download https://github.com/AdguardTeam/BlockYouTubeAdsShortcut/blob/master/index.js as youtube.js
+2. Debrand script as requested by AdguardTeam. Comment the following line within the script:
+ logo.innerHTML = '__logo_text__';
+
+ Currently at line 393.
diff --git a/third-party/adguard/adguard.gresource.xml b/third-party/adguard/adguard.gresource.xml
new file mode 100644
index 000000000..9e2f5b1c3
--- /dev/null
+++ b/third-party/adguard/adguard.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/epiphany/adguard">
+ <file compressed="true">youtube.js</file>
+ </gresource>
+</gresources>
diff --git a/third-party/adguard/youtube.js b/third-party/adguard/youtube.js
new file mode 100644
index 000000000..d9a3397ec
--- /dev/null
+++ b/third-party/adguard/youtube.js
@@ -0,0 +1,466 @@
+/**
+ * This file is part of AdGuard's Block YouTube Ads (https://github.com/AdguardTeam/BlockYouTubeAdsShortcut).
+ *
+ * AdGuard's Block YouTube Ads 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.
+ *
+ * AdGuard's Block YouTube Ads 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 AdGuard's Block YouTube Ads. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* global Response, window, navigator, document, MutationObserver, completion */
+
+/**
+ * The function that implements all the logic.
+ * Returns the run status.
+ */
+function runBlockYoutube() {
+ const locales = {
+ en: {
+ logo: 'with&nbsp;AdGuard',
+ alreadyExecuted: 'The shortcut has already been executed.',
+ wrongDomain: 'This shortcut is supposed to be launched only on YouTube.',
+ success: 'YouTube is now ad-free! Please note that you need to run this shortcut again if you reload the page.',
+ },
+ ru: {
+ logo: 'с&nbsp;AdGuard',
+ alreadyExecuted: 'Быстрая команда уже выполнена.',
+ wrongDomain: 'Эта быстрая команда предназначена для использования только на YouTube.',
+ success: 'Теперь YouTube без рекламы! Важно: при перезагрузке страницы вам нужно будет заново запустить команду.',
+ },
+ es: {
+ logo: 'con&nbsp;AdGuard',
+ alreadyExecuted: 'El atajo ya ha sido ejecutado.',
+ wrongDomain: 'Se supone que este atajo se lanza sólo en YouTube.',
+ success: '¡YouTube está ahora libre de anuncios! Ten en cuenta que tienes que volver a ejecutar este atajo si recargas la página.',
+ },
+ de: {
+ logo: 'mit&nbsp;AdGuard',
+ alreadyExecuted: 'Der Kurzbefehl wurde bereits ausgeführt.',
+ wrongDomain: 'Dieser Kurzbefehl soll nur auf YouTube gestartet werden.',
+ success: 'YouTube ist jetzt werbefrei! Bitte beachten Sie, dass Sie diesen Kurzbefehl erneut ausführen müssen, wenn Sie die Seite neu laden.',
+ },
+ fr: {
+ logo: 'avec&nbsp;AdGuard',
+ alreadyExecuted: 'Le raccourci a déjà été exécuté.',
+ wrongDomain: 'Ce raccourci est censé d’être lancé uniquement sur YouTube.',
+ success: 'YouTube est maintenant libre de pub ! Veuillez noter qu’il faudra rééxecuter le raccourci si vous rechargez la page.',
+ },
+ it: {
+ logo: 'con&nbsp;AdGuard',
+ alreadyExecuted: 'Il comando è già stato eseguito.',
+ wrongDomain: 'Questa scorciatoia dovrebbe essere lanciata solo su YouTube.',
+ success: 'YouTube è ora libero da pubblicità! Si prega di notare che è necessario eseguire nuovamente questa scorciatoia se ricarichi la pagina.',
+ },
+ 'zh-cn': {
+ logo: '使用&nbsp;AdGuard',
+ alreadyExecuted: '快捷指令已在运行',
+ wrongDomain: '快捷指令只能在 YouTube 上被启动。',
+ success: '现在您的 YouTube 没有广告!请注意,若您重新加载页面,您需要再次启动快捷指令。',
+ },
+ 'zh-tw': {
+ logo: '偕同&nbsp;AdGuard',
+ alreadyExecuted: '此捷徑已被執行。',
+ wrongDomain: '此捷徑應該只於 YouTube 上被啟動。',
+ success: '現在 YouTube 為無廣告的!請注意,若您重新載入該頁面,您需要再次執行此捷徑。',
+ },
+ ko: {
+ logo: 'AdGuard&nbsp;사용',
+ alreadyExecuted: '단축어가 이미 실행되었습니다.',
+ wrongDomain: '이 단축어는 YouTube에서만 사용 가능합니다.',
+ success: '이제 광고없이 YouTube를 시청할 수 있습니다. 페이지를 새로고침 할 경우, 이 단축어를 다시 실행해야 합니다.',
+ },
+ ja: {
+ logo: 'AdGuard作動中',
+ alreadyExecuted: 'ショートカットは既に実行されています。',
+ wrongDomain: '※このショートカットは、YouTubeでのみ適用されることを想定しています。',
+ success: 'YouTubeが広告なしになりました!※YouTubeページを再読み込みした場合は、このショートカットを再度実行する必要がありますのでご注意ください。',
+ },
+ uk: {
+ logo: 'з&nbsp;AdGuard',
+ alreadyExecuted: 'Ця швидка команда вже виконується.',
+ wrongDomain: 'Цю швидку команду слід запускати лише на YouTube.',
+ success: 'Тепер YouTube без реклами! Проте після перезавантаження сторінки необхідно знову запустити цю швидку команду.',
+ },
+ };
+
+ /**
+ * Gets a localized message for the specified key
+ *
+ * @param {string} key message key
+ * @returns {string} message for that key
+ */
+ const getMessage = (key) => {
+ try {
+ let locale = locales[navigator.language.toLowerCase()];
+ if (!locale) {
+ const lang = navigator.language.split('-')[0];
+ locale = locales[lang];
+ }
+ if (!locale) {
+ locale = locales.en;
+ }
+
+ return locale[key];
+ } catch (ex) {
+ return locales.en[key];
+ }
+ };
+
+ if (document.getElementById('block-youtube-ads-logo')) {
+ return {
+ success: false,
+ status: 'alreadyExecuted',
+ message: getMessage('alreadyExecuted'),
+ };
+ }
+
+ if (window.location.hostname !== 'www.youtube.com'
+ && window.location.hostname !== 'm.youtube.com'
+ && window.location.hostname !== 'music.youtube.com') {
+ return {
+ success: false,
+ status: 'wrongDomain',
+ message: getMessage('wrongDomain'),
+ };
+ }
+
+ /**
+ * Note that Shortcut scripts are executed in their own context (window)
+ * and we don't have direct access to the real page window.
+ *
+ * In order to overcome this, we add a "script" to the page which is
+ * executed in the proper context. The script content is inside
+ * the "pageScript" function.
+ */
+ const pageScript = () => {
+ const LOGO_ID = 'block-youtube-ads-logo';
+
+ const hiddenCSS = {
+ 'www.youtube.com': [
+ '#__ffYoutube1',
+ '#__ffYoutube2',
+ '#__ffYoutube3',
+ '#__ffYoutube4',
+ '#feed-pyv-container',
+ '#feedmodule-PRO',
+ '#homepage-chrome-side-promo',
+ '#merch-shelf',
+ '#offer-module',
+ '#pla-shelf > ytd-pla-shelf-renderer[class="style-scope ytd-watch"]',
+ '#pla-shelf',
+ '#premium-yva',
+ '#promo-info',
+ '#promo-list',
+ '#promotion-shelf',
+ '#related > ytd-watch-next-secondary-results-renderer > #items > ytd-compact-promoted-video-renderer.ytd-watch-next-secondary-results-renderer',
+ '#search-pva',
+ '#shelf-pyv-container',
+ '#video-masthead',
+ '#watch-branded-actions',
+ '#watch-buy-urls',
+ '#watch-channel-brand-div',
+ '#watch7-branded-banner',
+ '#YtKevlarVisibilityIdentifier',
+ '#YtSparklesVisibilityIdentifier',
+ '.carousel-offer-url-container',
+ '.companion-ad-container',
+ '.GoogleActiveViewElement',
+ '.list-view[style="margin: 7px 0pt;"]',
+ '.promoted-sparkles-text-search-root-container',
+ '.promoted-videos',
+ '.searchView.list-view',
+ '.sparkles-light-cta',
+ '.watch-extra-info-column',
+ '.watch-extra-info-right',
+ '.ytd-carousel-ad-renderer',
+ '.ytd-compact-promoted-video-renderer',
+ '.ytd-companion-slot-renderer',
+ '.ytd-merch-shelf-renderer',
+ '.ytd-player-legacy-desktop-watch-ads-renderer',
+ '.ytd-promoted-sparkles-text-search-renderer',
+ '.ytd-promoted-video-renderer',
+ '.ytd-search-pyv-renderer',
+ '.ytd-video-masthead-ad-v3-renderer',
+ '.ytp-ad-action-interstitial-background-container',
+ '.ytp-ad-action-interstitial-slot',
+ '.ytp-ad-image-overlay',
+ '.ytp-ad-overlay-container',
+ '.ytp-ad-progress',
+ '.ytp-ad-progress-list',
+ '[class*="ytd-display-ad-"]',
+ '[layout*="display-ad-"]',
+ 'a[href^="http://www.youtube.com/cthru?"]',
+ 'a[href^="https://www.youtube.com/cthru?"]',
+ 'ytd-action-companion-ad-renderer',
+ 'ytd-banner-promo-renderer',
+ 'ytd-compact-promoted-video-renderer',
+ 'ytd-companion-slot-renderer',
+ 'ytd-display-ad-renderer',
+ 'ytd-promoted-sparkles-text-search-renderer',
+ 'ytd-promoted-sparkles-web-renderer',
+ 'ytd-search-pyv-renderer',
+ 'ytd-single-option-survey-renderer',
+ 'ytd-video-masthead-ad-advertiser-info-renderer',
+ 'ytd-video-masthead-ad-v3-renderer',
+ 'YTM-PROMOTED-VIDEO-RENDERER',
+ ],
+ 'm.youtube.com': [
+ '.companion-ad-container',
+ '.ytp-ad-action-interstitial',
+ '.ytp-cued-thumbnail-overlay > div[style*="/sddefault.jpg"]',
+ 'a[href^="/watch?v="][onclick^="return koya.onEvent(arguments[0]||window.event,\'"]:not([role]):not([class]):not([id])',
+ 'a[onclick*=\'"ping_url":"http://www.google.com/aclk?\']',
+ 'ytm-companion-ad-renderer',
+ 'ytm-companion-slot',
+ 'ytm-promoted-sparkles-text-search-renderer',
+ 'ytm-promoted-sparkles-web-renderer',
+ 'ytm-promoted-video-renderer',
+ ],
+ };
+
+ /**
+ * Adds CSS to the page
+ * @param {string} hostname hostname
+ */
+ const hideElements = (hostname) => {
+ const selectors = hiddenCSS[hostname];
+ if (!selectors) {
+ return;
+ }
+ const rule = `${selectors.join(', ')} { display: none!important; }`;
+ const style = document.createElement('style');
+ style.innerHTML = rule;
+ document.head.appendChild(style);
+ };
+
+ /**
+ * Calls the "callback" function on every DOM change, but not for the tracked events
+ * @param {Function} callback callback function
+ */
+ const observeDomChanges = (callback) => {
+ const domMutationObserver = new MutationObserver((mutations) => {
+ callback(mutations);
+ });
+
+ domMutationObserver.observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ });
+ };
+
+ /**
+ * This function is supposed to be called on every DOM change
+ */
+ const hideDynamicAds = () => {
+ const elements = document.querySelectorAll('#contents > ytd-rich-item-renderer ytd-display-ad-renderer');
+ if (elements.length === 0) {
+ return;
+ }
+ elements.forEach((el) => {
+ if (el.parentNode && el.parentNode.parentNode) {
+ const parent = el.parentNode.parentNode;
+ if (parent.localName === 'ytd-rich-item-renderer') {
+ parent.style.display = 'none';
+ }
+ }
+ });
+ };
+
+ /**
+ * This function checks if the video ads are currently running
+ * and auto-clicks the skip button.
+ */
+ const autoSkipAds = () => {
+ // If there's a video that plays the ad at this moment, scroll this ad
+ if (document.querySelector('.ad-showing')) {
+ const video = document.querySelector('video');
+ if (video && video.duration) {
+ video.currentTime = video.duration;
+ // Skip button should appear after that,
+ // now simply click it automatically
+ setTimeout(() => {
+ const skipBtn = document.querySelector('button.ytp-ad-skip-button');
+ if (skipBtn) {
+ skipBtn.click();
+ }
+ }, 100);
+ }
+ }
+ };
+
+ /**
+ * This function overrides a property on the specified object.
+ *
+ * @param {object} obj object to look for properties in
+ * @param {string} propertyName property to override
+ * @param {*} overrideValue value to set
+ */
+ const overrideObject = (obj, propertyName, overrideValue) => {
+ if (!obj) {
+ return false;
+ }
+ let overriden = false;
+
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key) && key === propertyName) {
+ obj[key] = overrideValue;
+ overriden = true;
+ } else if (obj.hasOwnProperty(key) && typeof obj[key] === 'object') {
+ if (overrideObject(obj[key], propertyName, overrideValue)) {
+ overriden = true;
+ }
+ }
+ }
+
+ if (overriden) {
+ console.log(`found: ${propertyName}`);
+ }
+
+ return overriden;
+ };
+
+ /**
+ * Overrides JSON.parse and Response.json functions.
+ * Examines these functions arguments, looks for properties with the specified name there
+ * and if it exists, changes it's value to what was specified.
+ *
+ * @param {string} propertyName name of the property
+ * @param {*} overrideValue new value for the property
+ */
+ const jsonOverride = (propertyName, overrideValue) => {
+ const nativeJSONParse = JSON.parse;
+ JSON.parse = (...args) => {
+ const obj = nativeJSONParse.apply(this, args);
+
+ // Override it's props and return back to the caller
+ overrideObject(obj, propertyName, overrideValue);
+ return obj;
+ };
+
+ // Override Response.prototype.json
+ const nativeResponseJson = Response.prototype.json;
+ Response.prototype.json = new Proxy(nativeResponseJson, {
+ apply(...args) {
+ // Call the target function, get the original Promise
+ const promise = Reflect.apply(args);
+
+ // Create a new one and override the JSON inside
+ return new Promise((resolve, reject) => {
+ promise.then((data) => {
+ overrideObject(data, propertyName, overrideValue);
+ resolve(data);
+ }).catch((error) => reject(error));
+ });
+ },
+ });
+ };
+
+ const addAdGuardLogoStyle = () => {
+ const id = 'block-youtube-ads-logo-style';
+ if (document.getElementById(id)) {
+ return;
+ }
+
+ // Here is what these styles do:
+ // 1. Change AG marker color depending on the page
+ // 2. Hide Sign-in button on m.youtube.com otherwise it does not look good
+ // It is still possible to sign in by clicking "three dots" button.
+ // 3. Hide the marker when the user is searching for something
+ // 4. On YT Music apply display:block to the logo element
+ const style = document.createElement('style');
+ style.innerHTML = `[data-mode="watch"] #${LOGO_ID} { color: #fff; }
+[data-mode="searching"] #${LOGO_ID}, [data-mode="search"] #${LOGO_ID} { display: none; }
+#${LOGO_ID} { white-space: nowrap; }
+.mobile-topbar-header-sign-in-button { display: none; }
+.ytmusic-nav-bar#left-content #${LOGO_ID} { display: block; }`;
+ document.head.appendChild(style);
+ };
+
+ const addAdGuardLogo = () => {
+ if (document.getElementById(LOGO_ID)) {
+ return;
+ }
+
+ const logo = document.createElement('span');
+ // logo.innerHTML = '__logo_text__';
+ logo.setAttribute('id', LOGO_ID);
+
+ if (window.location.hostname === 'm.youtube.com') {
+ const btn = document.querySelector('header.mobile-topbar-header > button');
+ if (btn) {
+ btn.parentNode.insertBefore(logo, btn.nextSibling);
+ addAdGuardLogoStyle();
+ }
+ } else if (window.location.hostname === 'www.youtube.com') {
+ const code = document.getElementById('country-code');
+ if (code) {
+ code.innerHTML = '';
+ code.appendChild(logo);
+ addAdGuardLogoStyle();
+ }
+ } else if (window.location.hostname === 'music.youtube.com') {
+ const el = document.querySelector('.ytmusic-nav-bar#left-content');
+ if (el) {
+ el.appendChild(logo);
+ addAdGuardLogoStyle();
+ }
+ }
+ };
+
+ // Removes ads metadata from YouTube XHR requests
+ jsonOverride('adPlacements', []);
+ jsonOverride('playerAds', []);
+
+ // Applies CSS that hides YouTube ad elements
+ hideElements(window.location.hostname);
+
+ // Some changes should be re-evaluated on every page change
+ addAdGuardLogo();
+ hideDynamicAds();
+ autoSkipAds();
+ observeDomChanges(() => {
+ addAdGuardLogo();
+ hideDynamicAds();
+ autoSkipAds();
+ });
+ };
+
+ const script = document.createElement('script');
+ const scriptText = pageScript.toString().replace('__logo_text__', getMessage('logo'));
+ script.innerHTML = `(${scriptText})();`;
+ document.head.appendChild(script);
+ document.head.removeChild(script);
+
+ return {
+ success: true,
+ status: 'success',
+ message: getMessage('success'),
+ };
+}
+
+/**
+ * Runs the shortcut
+ */
+(() => {
+ // "completion" function is only defined if this script is launched as Shortcut
+ // in other cases we simply polyfill it.
+ let finish = (m) => { console.log(m); };
+ if (typeof completion !== 'undefined') {
+ finish = completion;
+ }
+
+ try {
+ const result = runBlockYoutube();
+ finish(result.message);
+ } catch (ex) {
+ finish(ex.toString());
+ }
+})();
diff --git a/third-party/meson.build b/third-party/meson.build
index 43bd89311..d65b8747e 100644
--- a/third-party/meson.build
+++ b/third-party/meson.build
@@ -1,3 +1,10 @@
+adguard_resource_files = files('adguard/adguard.gresource.xml')
+adguard_resources = gnome.compile_resources('adguard-resources',
+ adguard_resource_files,
+ c_name: 'adguard',
+ source_dir: 'adguard'
+)
+
pdfjs_resource_files = files('pdfjs/pdfjs.gresource.xml')
pdfjs_resources = gnome.compile_resources('pdfjs-resources',
pdfjs_resource_files,