/******************************************************************************* uBlock Origin - a comprehensive, efficient content blocker Copyright (C) 2019-present Raymond Hill This program 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. This program 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 this program. If not, see {http://www.gnu.org/licenses/}. Home: https://github.com/gorhill/uBlock The scriptlets below are meant to be injected only into a web page context. */ import { registerScriptlet } from './base.js'; import { safeSelf } from './safe-self.js'; /******************************************************************************/ export function getSafeCookieValuesFn() { return [ 'accept', 'reject', 'accepted', 'rejected', 'notaccepted', 'allow', 'disallow', 'deny', 'allowed', 'denied', 'approved', 'disapproved', 'checked', 'unchecked', 'dismiss', 'dismissed', 'enable', 'disable', 'enabled', 'disabled', 'essential', 'nonessential', 'forbidden', 'forever', 'hide', 'hidden', 'necessary', 'required', 'ok', 'on', 'off', 'true', 't', 'false', 'f', 'yes', 'y', 'no', 'n', ]; } registerScriptlet(getSafeCookieValuesFn, { name: 'get-safe-cookie-values.fn', }); /******************************************************************************/ export function getAllCookiesFn() { return document.cookie.split(/\s*;\s*/).map(s => { const pos = s.indexOf('='); if ( pos === 0 ) { return; } if ( pos === -1 ) { return `${s.trim()}=`; } const key = s.slice(0, pos).trim(); const value = s.slice(pos+1).trim(); return { key, value }; }).filter(s => s !== undefined); } registerScriptlet(getAllCookiesFn, { name: 'get-all-cookies.fn', }); /******************************************************************************/ export function getCookieFn( name = '' ) { for ( const s of document.cookie.split(/\s*;\s*/) ) { const pos = s.indexOf('='); if ( pos === -1 ) { continue; } if ( s.slice(0, pos) !== name ) { continue; } return s.slice(pos+1).trim(); } } registerScriptlet(getCookieFn, { name: 'get-cookie.fn', }); /******************************************************************************/ export function setCookieFn( trusted = false, name = '', value = '', expires = '', path = '', options = {}, ) { // https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 // https://github.com/uBlockOrigin/uBlock-issues/issues/2777 if ( trusted === false && /[^!#$%&'*+\-.0-9A-Z[\]^_`a-z|~]/.test(name) ) { name = encodeURIComponent(name); } // https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1 // The characters [",] are given a pass from the RFC requirements because // apparently browsers do not follow the RFC to the letter. if ( /[^ -:<-[\]-~]/.test(value) ) { value = encodeURIComponent(value); } const cookieBefore = getCookieFn(name); if ( cookieBefore !== undefined && options.dontOverwrite ) { return; } if ( cookieBefore === value && options.reload ) { return; } const cookieParts = [ name, '=', value ]; if ( expires !== '' ) { cookieParts.push('; expires=', expires); } if ( path === '' ) { path = '/'; } else if ( path === 'none' ) { path = ''; } if ( path !== '' && path !== '/' ) { return; } if ( path === '/' ) { cookieParts.push('; path=/'); } if ( trusted ) { if ( options.domain ) { cookieParts.push(`; domain=${options.domain}`); } cookieParts.push('; Secure'); } else if ( /^__(Host|Secure)-/.test(name) ) { cookieParts.push('; Secure'); } try { document.cookie = cookieParts.join(''); } catch(_) { } const done = getCookieFn(name) === value; if ( done && options.reload ) { window.location.reload(); } return done; } registerScriptlet(setCookieFn, { name: 'set-cookie.fn', dependencies: [ getCookieFn, ], }); /** * @scriptlet set-cookie * * @description * Set a cookie to a safe value. * * @param name * The name of the cookie to set. * * @param value * The value of the cookie to set. Must be a safe value. Unsafe values will be * ignored and no cookie will be set. See getSafeCookieValuesFn() helper above. * * @param [path] * Optional. The path of the cookie to set. Default to `/`. * * Reference: * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-cookie.js * */ export function setCookie( name = '', value = '', path = '' ) { if ( name === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path); const normalized = value.toLowerCase(); const match = /^("?)(.+)\1$/.exec(normalized); const unquoted = match && match[2] || normalized; const validValues = getSafeCookieValuesFn(); if ( validValues.includes(unquoted) === false ) { if ( /^-?\d+$/.test(unquoted) === false ) { return; } const n = parseInt(value, 10) || 0; if ( n < -32767 || n > 32767 ) { return; } } const done = setCookieFn( false, name, value, '', path, safe.getExtraArgs(Array.from(arguments), 3) ); if ( done ) { safe.uboLog(logPrefix, 'Done'); } } registerScriptlet(setCookie, { name: 'set-cookie.js', world: 'ISOLATED', dependencies: [ getSafeCookieValuesFn, safeSelf, setCookieFn, ], }); // For compatibility with AdGuard export function setCookieReload(name, value, path, ...args) { setCookie(name, value, path, 'reload', '1', ...args); } registerScriptlet(setCookieReload, { name: 'set-cookie-reload.js', world: 'ISOLATED', dependencies: [ setCookie, ], }); /** * @trustedScriptlet trusted-set-cookie * * @description * Set a cookie to any value. This scriptlet can be used only from a trusted * source. * * @param name * The name of the cookie to set. * * @param value * The value of the cookie to set. Must be a safe value. Unsafe values will be * ignored and no cookie will be set. See getSafeCookieValuesFn() helper above. * * @param [offsetExpiresSec] * Optional. The path of the cookie to set. Default to `/`. * * @param [path] * Optional. The path of the cookie to set. Default to `/`. * * Reference: * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-cookie.js * */ export function trustedSetCookie( name = '', value = '', offsetExpiresSec = '', path = '' ) { if ( name === '' ) { return; } const safe = safeSelf(); const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path); const time = new Date(); if ( value.includes('$now$') ) { value = value.replaceAll('$now$', time.getTime()); } if ( value.includes('$currentDate$') ) { value = value.replaceAll('$currentDate$', time.toUTCString()); } if ( value.includes('$currentISODate$') ) { value = value.replaceAll('$currentISODate$', time.toISOString()); } let expires = ''; if ( offsetExpiresSec !== '' ) { if ( offsetExpiresSec === '1day' ) { time.setDate(time.getDate() + 1); } else if ( offsetExpiresSec === '1year' ) { time.setFullYear(time.getFullYear() + 1); } else { if ( /^\d+$/.test(offsetExpiresSec) === false ) { return; } time.setSeconds(time.getSeconds() + parseInt(offsetExpiresSec, 10)); } expires = time.toUTCString(); } const done = setCookieFn( true, name, value, expires, path, safeSelf().getExtraArgs(Array.from(arguments), 4) ); if ( done ) { safe.uboLog(logPrefix, 'Done'); } } registerScriptlet(trustedSetCookie, { name: 'trusted-set-cookie.js', requiresTrust: true, world: 'ISOLATED', dependencies: [ safeSelf, setCookieFn, ], }); // For compatibility with AdGuard export function trustedSetCookieReload(name, value, offsetExpiresSec, path, ...args) { trustedSetCookie(name, value, offsetExpiresSec, path, 'reload', '1', ...args); } registerScriptlet(trustedSetCookieReload, { name: 'trusted-set-cookie-reload.js', requiresTrust: true, world: 'ISOLATED', dependencies: [ trustedSetCookie, ], }); /** * @scriptlet remove-cookie * * @description * Removes current site cookies specified by name. The removal operation occurs * immediately when the scriptlet is injected, then when the page is unloaded. * * @param needle * A string or a regex matching the name of the cookie(s) to remove. * * @param ['when', token] * Vararg, optional. The parameter following 'when' tells when extra removal * operations should take place. * - `scroll`: when the page is scrolled * - `keydown`: when a keyboard touch is pressed * * */ export function removeCookie( needle = '' ) { if ( typeof needle !== 'string' ) { return; } const safe = safeSelf(); const reName = safe.patternToRegex(needle); const extraArgs = safe.getExtraArgs(Array.from(arguments), 1); const throttle = (fn, ms = 500) => { if ( throttle.timer !== undefined ) { return; } throttle.timer = setTimeout(( ) => { throttle.timer = undefined; fn(); }, ms); }; const remove = ( ) => { document.cookie.split(';').forEach(cookieStr => { const pos = cookieStr.indexOf('='); if ( pos === -1 ) { return; } const cookieName = cookieStr.slice(0, pos).trim(); if ( reName.test(cookieName) === false ) { return; } const part1 = cookieName + '='; const part2a = '; domain=' + document.location.hostname; const part2b = '; domain=.' + document.location.hostname; let part2c, part2d; const domain = document.domain; if ( domain ) { if ( domain !== document.location.hostname ) { part2c = '; domain=.' + domain; } if ( domain.startsWith('www.') ) { part2d = '; domain=' + domain.replace('www', ''); } } const part3 = '; path=/'; const part4 = '; Max-Age=-1000; expires=Thu, 01 Jan 1970 00:00:00 GMT'; document.cookie = part1 + part4; document.cookie = part1 + part2a + part4; document.cookie = part1 + part2b + part4; document.cookie = part1 + part3 + part4; document.cookie = part1 + part2a + part3 + part4; document.cookie = part1 + part2b + part3 + part4; if ( part2c !== undefined ) { document.cookie = part1 + part2c + part3 + part4; } if ( part2d !== undefined ) { document.cookie = part1 + part2d + part3 + part4; } }); }; remove(); window.addEventListener('beforeunload', remove); if ( typeof extraArgs.when !== 'string' ) { return; } const supportedEventTypes = [ 'scroll', 'keydown' ]; const eventTypes = extraArgs.when.split(/\s/); for ( const type of eventTypes ) { if ( supportedEventTypes.includes(type) === false ) { continue; } document.addEventListener(type, ( ) => { throttle(remove); }, { passive: true }); } } registerScriptlet(removeCookie, { name: 'remove-cookie.js', aliases: [ 'cookie-remover.js', ], world: 'ISOLATED', dependencies: [ safeSelf, ], }); /******************************************************************************/