uBlock/assets/resources/cookie.js

412 lines
12 KiB
JavaScript
Raw Normal View History

/*******************************************************************************
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,
],
});
/******************************************************************************/