mirror of https://github.com/gorhill/uBlock.git
411 lines
12 KiB
JavaScript
411 lines
12 KiB
JavaScript
|
/*******************************************************************************
|
||
|
|
||
|
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 { 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',
|
||
|
];
|
||
|
}
|
||
|
getSafeCookieValuesFn.details = {
|
||
|
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);
|
||
|
}
|
||
|
getAllCookiesFn.details = {
|
||
|
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();
|
||
|
}
|
||
|
}
|
||
|
getCookieFn.details = {
|
||
|
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;
|
||
|
}
|
||
|
setCookieFn.details = {
|
||
|
name: 'set-cookie.fn',
|
||
|
dependencies: [
|
||
|
'get-cookie.fn',
|
||
|
],
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @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');
|
||
|
}
|
||
|
}
|
||
|
setCookie.details = {
|
||
|
name: 'set-cookie.js',
|
||
|
world: 'ISOLATED',
|
||
|
dependencies: [
|
||
|
'get-safe-cookie-values.fn',
|
||
|
'safe-self.fn',
|
||
|
'set-cookie.fn',
|
||
|
],
|
||
|
};
|
||
|
|
||
|
// For compatibility with AdGuard
|
||
|
export function setCookieReload(name, value, path, ...args) {
|
||
|
setCookie(name, value, path, 'reload', '1', ...args);
|
||
|
}
|
||
|
setCookieReload.details = {
|
||
|
name: 'set-cookie-reload.js',
|
||
|
world: 'ISOLATED',
|
||
|
dependencies: [
|
||
|
'set-cookie.js',
|
||
|
],
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @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');
|
||
|
}
|
||
|
}
|
||
|
trustedSetCookie.details = {
|
||
|
name: 'trusted-set-cookie.js',
|
||
|
requiresTrust: true,
|
||
|
world: 'ISOLATED',
|
||
|
dependencies: [
|
||
|
'safe-self.fn',
|
||
|
'set-cookie.fn',
|
||
|
],
|
||
|
};
|
||
|
|
||
|
// For compatibility with AdGuard
|
||
|
export function trustedSetCookieReload(name, value, offsetExpiresSec, path, ...args) {
|
||
|
trustedSetCookie(name, value, offsetExpiresSec, path, 'reload', '1', ...args);
|
||
|
}
|
||
|
trustedSetCookieReload.details = {
|
||
|
name: 'trusted-set-cookie-reload.js',
|
||
|
requiresTrust: true,
|
||
|
world: 'ISOLATED',
|
||
|
dependencies: [
|
||
|
'trusted-set-cookie.js',
|
||
|
],
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @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 });
|
||
|
}
|
||
|
}
|
||
|
removeCookie.details = {
|
||
|
name: 'remove-cookie.js',
|
||
|
aliases: [
|
||
|
'cookie-remover.js',
|
||
|
],
|
||
|
world: 'ISOLATED',
|
||
|
dependencies: [
|
||
|
'safe-self.fn',
|
||
|
],
|
||
|
};
|
||
|
|
||
|
/******************************************************************************/
|