diff options
author | Jan-Michael Brummer <jan.brummer@tabos.org> | 2021-08-10 14:46:39 +0200 |
---|---|---|
committer | Michael Catanzaro <mcatanzaro@gnome.org> | 2021-08-12 19:25:01 +0000 |
commit | abd396a7f8c70100f9604c77a3ff23905703cadc (patch) | |
tree | 83dc76f16475a4944fc7d3d5fbdd3d90bb563ed4 | |
parent | b2829e2b8a20f13d3348d5dd3af0d47e66cb1a0d (diff) | |
download | epiphany-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.c | 30 | ||||
-rw-r--r-- | src/meson.build | 1 | ||||
-rw-r--r-- | tests/meson.build | 2 | ||||
-rw-r--r-- | third-party/adguard/README.epiphany.md | 10 | ||||
-rw-r--r-- | third-party/adguard/adguard.gresource.xml | 6 | ||||
-rw-r--r-- | third-party/adguard/youtube.js | 466 | ||||
-rw-r--r-- | third-party/meson.build | 7 |
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 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: 'с AdGuard', + alreadyExecuted: 'Быстрая команда уже выполнена.', + wrongDomain: 'Эта быстрая команда предназначена для использования только на YouTube.', + success: 'Теперь YouTube без рекламы! Важно: при перезагрузке страницы вам нужно будет заново запустить команду.', + }, + es: { + logo: 'con 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 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 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 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: '使用 AdGuard', + alreadyExecuted: '快捷指令已在运行', + wrongDomain: '快捷指令只能在 YouTube 上被启动。', + success: '现在您的 YouTube 没有广告!请注意,若您重新加载页面,您需要再次启动快捷指令。', + }, + 'zh-tw': { + logo: '偕同 AdGuard', + alreadyExecuted: '此捷徑已被執行。', + wrongDomain: '此捷徑應該只於 YouTube 上被啟動。', + success: '現在 YouTube 為無廣告的!請注意,若您重新載入該頁面,您需要再次執行此捷徑。', + }, + ko: { + logo: 'AdGuard 사용', + alreadyExecuted: '단축어가 이미 실행되었습니다.', + wrongDomain: '이 단축어는 YouTube에서만 사용 가능합니다.', + success: '이제 광고없이 YouTube를 시청할 수 있습니다. 페이지를 새로고침 할 경우, 이 단축어를 다시 실행해야 합니다.', + }, + ja: { + logo: 'AdGuard作動中', + alreadyExecuted: 'ショートカットは既に実行されています。', + wrongDomain: '※このショートカットは、YouTubeでのみ適用されることを想定しています。', + success: 'YouTubeが広告なしになりました!※YouTubeページを再読み込みした場合は、このショートカットを再度実行する必要がありますのでご注意ください。', + }, + uk: { + logo: 'з 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, |