Merge branch 'build/1.x' into production

This commit is contained in:
Ilya Ig. Petrov 2017-06-09 08:00:09 +05:00
commit 336ec6c6bb
53 changed files with 8113 additions and 1512 deletions

View File

@ -10,10 +10,10 @@ This repo contains:
[WebStore](https://chrome.google.com/webstore/detail/npgcnondjocldhldegnakemclmfkngch) [WebStore](https://chrome.google.com/webstore/detail/npgcnondjocldhldegnakemclmfkngch)
| [Sources](https://github.com/ilyaigpetrov/anti-censorship-russia/tree/master/extensions/chromium/minimalistic-pac-setter) | [Sources](https://github.com/ilyaigpetrov/anti-censorship-russia/tree/master/extensions/chromium/minimalistic-pac-setter)
2. Proof of concept PAC-script generator based on https://github.com/zapret-info/z-i 2. Proof of concept PAC-script generator based on https://github.com/zapret-info/z-i
3. PAC-scripts performance analyses of scripts generated 3. ~~PAC-scripts performance analyses of scripts generated~~ (doesn't take parse time into account)
4. Based on the research of step 3 [the final PAC-generator][pac-generator] was written as a Google App Script in JavaScript which is triggered every two hours to generate and publish PAC-script on Google Drive (don't use direct URL without extension, please, URL will be periodically changed to counter abuse). 4. Based on the research of step 3 [the final PAC-generator][pac-generator] was written as a Google App Script in JavaScript which is triggered every two hours to generate and publish PAC-script on Google Drive (don't use direct URL without extension, please, URL will be periodically changed to counter abuse).
[pac-generator]: https://script.google.com/d/18EG6_pPuSqzJaCU8ePzt_VfbvBI2maBIr5O8EMfktkBd5NNYKv8VvG4Y/edit?usp=sharing [pac-generator]: https://script.google.com/d/1iSwFilpiahetZ9hNPUK5SATjrNoZ_4i7aV9TWSp58LBhcyg_TuK_Qb5S/edit?usp=sharing
## Why I do This ## Why I do This

View File

@ -1,4 +1,6 @@
node_modules node_modules
node_modules_linux
node_modules_win
npm-debug.log npm-debug.log
.swp *.swp
build/ build/

View File

@ -4,6 +4,7 @@ const gulp = require('gulp');
const del = require('del'); const del = require('del');
const through = require('through2'); const through = require('through2');
const PluginError = require('gulp-util').PluginError; const PluginError = require('gulp-util').PluginError;
const changed = require('gulp-changed');
const PluginName = 'Template literals'; const PluginName = 'Template literals';
@ -44,39 +45,61 @@ const templatePlugin = (context) => through.obj(function(file, encoding, cb) {
gulp.task('default', ['build']); gulp.task('default', ['build']);
gulp.task('clean', function() { gulp.task('clean', function(cb) {
return del.sync('./build'); //return del.sync('./build');
return cb();
}); });
const contexts = require('./src/templates-data').contexts; const contexts = require('./src/templates-data').contexts;
gulp.task('_cp-common', ['clean'], function() { const excFolder = (name) => [`!./src/**/${name}`, `!./src/**/${name}/**/*`];
const excluded = [ ...excFolder('test') , ...excFolder('node_modules'), ...excFolder('src') ];
const commonWoTests = ['./src/extension-common/**/*', ...excluded];
gulp.src(['./src/extension-common/**/*']) const miniDst = './build/extension-mini';
const fullDst = './build/extension-full';
gulp.task('_cp-common', ['clean'], function(cb) {
let fins = 0;
const intheend = () => {
if (++fins === 2) {
cb();
}
};
gulp.src(commonWoTests)
.pipe(changed(miniDst))
.pipe(templatePlugin(contexts.mini)) .pipe(templatePlugin(contexts.mini))
.pipe(gulp.dest('./build/extension-mini')) .pipe(gulp.dest(miniDst))
.on('end', intheend);
gulp.src(['./src/extension-common/**/*']) gulp.src(commonWoTests)
.pipe(changed(fullDst))
.pipe(templatePlugin(contexts.full)) .pipe(templatePlugin(contexts.full))
.pipe(gulp.dest('./build/extension-full')); .pipe(gulp.dest(fullDst))
.on('end', intheend);
}); });
gulp.task('_cp-mini', ['_cp-common'], function() { gulp.task('_cp-mini', ['_cp-common'], function(cb) {
gulp.src(['./src/extension-mini/**/*']) gulp.src(['./src/extension-mini/**/*', ...excluded])
.pipe(changed(miniDst))
.pipe(templatePlugin(contexts.mini)) .pipe(templatePlugin(contexts.mini))
.pipe(gulp.dest('./build/extension-mini')); .pipe(gulp.dest(miniDst))
.on('end', cb);
}); });
gulp.task('_cp-full', ['_cp-common'], function() { gulp.task('_cp-full', ['_cp-common'], function(cb) {
gulp.src(['./src/extension-full/**/*']) gulp.src(['./src/extension-full/**/*', ...excluded])
.pipe(changed(fullDst))
.pipe(templatePlugin(contexts.full)) .pipe(templatePlugin(contexts.full))
.pipe(gulp.dest('./build/extension-full')); .pipe(gulp.dest(fullDst))
.on('end', cb);
}); });

View File

@ -3,14 +3,21 @@
"version": "0.0.19", "version": "0.0.19",
"description": "Development tools for chromium extension", "description": "Development tools for chromium extension",
"scripts": { "scripts": {
"postinstall": "node --use_strict -e \"const fs = require('fs'), path = 'node_modules/_project-root'; fs.unlink(path, ()=> fs.symlinkSync('..', path, 'dir'));\"",
"lint": "eslint ./src/**/*.js --ignore-pattern vendor", "lint": "eslint ./src/**/*.js --ignore-pattern vendor",
"gulp": "gulp" "gulp": "gulp",
"test": "mocha --recursive ./src/**/test/*",
"start": "cd ./src/extension-common/pages/options/ && npm run build && cd - && npm run gulp"
}, },
"author": "Ilya Ig. Petrov", "author": "Ilya Ig. Petrov",
"license": "GPLv3", "license": "GPLv3",
"devDependencies": { "devDependencies": {
"chai": "^3.5.0",
"eslint": "^3.15.0", "eslint": "^3.15.0",
"eslint-config-google": "^0.7.1" "eslint-config-google": "^0.7.1",
"gulp-changed": "^3.1.0",
"mocha": "^3.3.0",
"sinon-chrome": "^2.2.1"
}, },
"dependencies": { "dependencies": {
"del": "^2.2.2", "del": "^2.2.2",

View File

@ -125,16 +125,16 @@
key = prefix + key; key = prefix + key;
if (value === null) { if (value === null) {
return localStorage.removeItem(key); return window.localStorage.removeItem(key);
} }
if (value === undefined) { if (value === undefined) {
const item = localStorage.getItem(key); const item = window.localStorage.getItem(key);
return item && JSON.parse(item); return item && JSON.parse(item);
} }
if (value instanceof Date) { if (value instanceof Date) {
throw new TypeError('Converting Date format to JSON is not supported.'); throw new TypeError('Converting Date format to JSON is not supported.');
} }
localStorage.setItem(key, JSON.stringify(value)); window.localStorage.setItem(key, JSON.stringify(value));
}; };
@ -188,6 +188,7 @@
window.apis = { window.apis = {
version: { version: {
ifMini: false, ifMini: false,
build: chrome.runtime.getManifest().version.replace(/\d+\.\d+\./g, ''),
}, },
}; };

View File

@ -61,7 +61,7 @@
const ifPrefix = 'if-on-'; const ifPrefix = 'if-on-';
const extName = chrome.runtime.getManifest().name; const extName = chrome.runtime.getManifest().name;
const extVersion = chrome.runtime.getManifest().version.replace(/\d+\.\d+\./g, ''); const extVersion = window.apis.version.build;
window.apis.errorHandlers = { window.apis.errorHandlers = {

View File

@ -12,66 +12,79 @@
const ifIncontinence = 'if-incontinence'; const ifIncontinence = 'if-incontinence';
const modsKey = 'mods'; const modsKey = 'mods';
// Don't keep objects in defaults or at least freeze them! const getDefaultConfigs = () => ({// Configs user may mutate them and we don't care!
const configs = {
ifProxyHttpsUrlsOnly: { ifProxyHttpsUrlsOnly: {
dflt: false, dflt: false,
label: 'проксировать только HTTP<em>S</em>-сайты', label: 'проксировать только HTTP<em>S</em>-сайты',
desc: 'Проксировать только сайты, доступные по шифрованному протоколу HTTP<em>S</em>. Прокси и провайдер смогут видеть только адреса проксируемых HTTP<em>S</em>-сайтов, но не их содержимое. Используйте, если вы не доверяете прокси-серверам ваш HTTP-трафик. Разумеется, что с этой опцией разблокировка HTTP-сайтов работать не будет.', desc: 'Проксировать только сайты, доступные по шифрованному протоколу HTTP<em>S</em>. Прокси и провайдер смогут видеть только адреса проксируемых HTTP<em>S</em>-сайтов, но не их содержимое. Используйте, если вы не доверяете прокси-серверам ваш HTTP-трафик. Разумеется, что с этой опцией разблокировка HTTP-сайтов работать не будет.',
index: 0, order: 0,
}, },
ifUseSecureProxiesOnly: { ifUseSecureProxiesOnly: {
dflt: false, dflt: false,
label: 'только шифрованная связь с прокси', label: 'только шифрованная связь с прокси',
desc: 'Шифровать соединение до прокси от провайдера, используя только прокси типа HTTPS или локальный Tor. Провайдер всё же сможет видеть адреса (но не содержимое) проксируемых ресурсов из протокола DNS (даже с Tor). Опция вряд ли может быть вам полезна, т.к. шифруется не весь трафик, а лишь разблокируемые ресурсы.', desc: 'Шифровать соединение до прокси от провайдера, используя только прокси типа HTTPS или локальный Tor. Провайдер всё же сможет видеть адреса (но не содержимое) проксируемых ресурсов из протокола DNS (даже с Tor). Опция вряд ли может быть вам полезна, т.к. шифруется не весь трафик, а лишь разблокируемые ресурсы.',
index: 1, order: 1,
}, },
ifProhibitDns: { ifProhibitDns: {
dflt: false, dflt: false,
label: 'запретить опредление по IP/DNS', label: 'запретить опредление по IP/DNS',
desc: 'Пытается запретить скрипту использовать DNS, без которого определение блокировки по IP работать не будет (т.е. будет разблокироваться меньше сайтов). Используйте, чтобы получить прирост в производительности или если вам кажется, что мы проксируем слишком много сайтов. Запрет действует только для скрипта, браузер и др.программы продолжат использование DNS.', desc: 'Пытается запретить скрипту использовать DNS, без которого определение блокировки по IP работать не будет (т.е. будет разблокироваться меньше сайтов). Используйте, чтобы получить прирост в производительности или если вам кажется, что мы проксируем слишком много сайтов. Запрет действует только для скрипта, браузер и др.программы продолжат использование DNS.',
index: 2, order: 2,
}, },
ifProxyOrDie: { ifProxyOrDie: {
dflt: true, dflt: true,
ifDfltMods: true, ifDfltMods: true,
label: 'проксируй или умри!', label: 'проксируй или умри!',
desc: 'Запрещает соединение с сайтами напрямую без прокси в случаях, когда все прокси отказывают. Например, если все ВАШИ прокси вдруг недоступны, то добавленные вручную сайты открываться не будут совсем. Однако смысл опции в том, что она препятствует занесению прокси в чёрные списки Хрома. Рекомендуется не отключать.', desc: 'Запрещает соединение с сайтами напрямую без прокси в случаях, когда все прокси отказывают. Например, если все ВАШИ прокси вдруг недоступны, то добавленные вручную сайты открываться не будут совсем. Однако смысл опции в том, что она препятствует занесению прокси в чёрные списки Хрома. Рекомендуется не отключать.',
index: 3, order: 3,
}, },
ifUsePacScriptProxies: { ifUsePacScriptProxies: {
dflt: true, dflt: true,
category: 'ownProxies',
label: 'использовать прокси PAC-скрипта', label: 'использовать прокси PAC-скрипта',
desc: 'Использовать прокси-сервера от авторов PAC-скрипта.', desc: 'Использовать прокси-сервера от авторов PAC-скрипта.',
index: 4, order: 4,
}, },
ifUseLocalTor: { ifUseLocalTor: {
dflt: false, dflt: false,
category: 'ownProxies',
label: 'использовать СВОЙ локальный Tor', label: 'использовать СВОЙ локальный Tor',
desc: 'Установите <a href="https://rebrand.ly/ac-tor">Tor</a> на свой компьютер и используйте его как прокси-сервер. <a href="https://rebrand.ly/ac-tor">ВАЖНО</a>', desc: 'Установите <a href="https://rebrand.ly/ac-tor">Tor</a> на свой компьютер и используйте его как прокси-сервер. <a href="https://rebrand.ly/ac-tor">ВАЖНО</a>',
index: 5, order: 5,
}, },
exceptions: { exceptions: {
category: 'exceptions',
dflt: null, dflt: null,
}, },
ifMindExceptions: { ifMindExceptions: {
dflt: true, dflt: true,
category: 'exceptions',
label: 'учитывать исключения', label: 'учитывать исключения',
desc: 'Учитывать сайты, добавленные вручную. Только для своих прокси-серверов! Без своих прокси работать не будет.', desc: 'Учитывать сайты, добавленные вручную. Только для своих прокси-серверов! Без своих прокси работать не будет.',
index: 6, order: 6,
}, },
customProxyStringRaw: { customProxyStringRaw: {
dflt: '', dflt: '',
category: 'ownProxies',
label: 'использовать СВОИ прокси', label: 'использовать СВОИ прокси',
url: 'https://rebrand.ly/ac-own-proxy', url: 'https://rebrand.ly/ac-own-proxy',
index: 7, order: 7,
},
ifProxyMoreDomains: {
ifDisabled: true,
dflt: false,
category: 'ownProxies',
label: 'проксировать .onion, .i2p и <a href="https://en.wikipedia.org/wiki/OpenNIC#OpenNIC_TLDs">OpenNIC</a>',
desc: 'Проксировать особые домены. Необходима поддержка со стороны СВОИХ прокси.',
order: 8,
}, },
}; });
const getDefaults = function getDefaults() { const getDefaults = function getDefaults() {
const configs = getDefaultConfigs();
return Object.keys(configs).reduce((acc, key) => { return Object.keys(configs).reduce((acc, key) => {
acc[key] = configs[key].dflt; acc[key] = configs[key].dflt;
@ -83,7 +96,15 @@
const getCurrentConfigs = function getCurrentConfigs() { const getCurrentConfigs = function getCurrentConfigs() {
const [err, mods, ...warns] = createPacModifiers( kitchenState(modsKey) ); const oldMods = kitchenState(modsKey);
/*if (oldMods) {
// No migration!
return oldMods;
}*/
// Client may expect mods.included and mods.excluded!
// On first install they are not defined.
const [err, mods, ...warns] = createPacModifiers(oldMods);
if (err) { if (err) {
throw err; throw err;
} }
@ -91,16 +112,22 @@
}; };
const getOrderedConfigsForUser = function getOrderedConfigs() { const getOrderedConfigsForUser = function getOrderedConfigs(category) {
const pacMods = getCurrentConfigs(); const pacMods = getCurrentConfigs();
return Object.keys(configs).reduce((arr, key) => { const configs = getDefaultConfigs();
return Object.keys(configs)
.sort((keyA, keyB) => configs[keyA].order - configs[keyB].order)
.reduce((arr, key) => {
const conf = configs[key]; const conf = configs[key];
if(typeof(conf.index) === 'number') { if(typeof(conf.order) === 'number') {
arr[conf.index] = conf; if(!category || category === (conf.category || 'general')) {
conf.value = pacMods[key]; conf.value = pacMods[key];
conf.key = key; conf.key = key;
conf.category = category || 'general';
arr.push(conf);
}
} }
return arr; return arr;
@ -111,6 +138,7 @@
const createPacModifiers = function createPacModifiers(mods = {}) { const createPacModifiers = function createPacModifiers(mods = {}) {
mods = mods || {}; // null? mods = mods || {}; // null?
const configs = getDefaultConfigs();
const ifNoMods = Object.keys(configs) const ifNoMods = Object.keys(configs)
.every((dProp) => { .every((dProp) => {
@ -123,7 +151,6 @@
}); });
console.log('Input mods:', mods);
const self = {}; const self = {};
Object.assign(self, getDefaults(), mods); Object.assign(self, getDefaults(), mods);
self.ifNoMods = ifNoMods; self.ifNoMods = ifNoMods;
@ -140,7 +167,8 @@
} }
} }
if (self.ifUseLocalTor) { if (self.ifUseLocalTor) {
customProxyArray.push('SOCKS5 localhost:9050', 'SOCKS5 localhost:9150'); self.torPoints = ['SOCKS5 localhost:9150', 'SOCKS5 localhost:9050'];
customProxyArray.push(...self.torPoints);
} }
self.filteredCustomsString = ''; self.filteredCustomsString = '';
@ -154,7 +182,18 @@
self.customProxyArray = false; self.customProxyArray = false;
} }
self.included = self.excluded = undefined; [self.included, self.excluded] = [[], []];
if (self.ifProxyMoreDomains) {
self.moreDomains = [
/* Networks */
'onion', 'i2p',
/* OpenNIC */
'bbs', 'chan', 'dyn', 'free', 'geek', 'gopher', 'indy',
'libre', 'neo', 'null', 'o', 'oss', 'oz', 'parody', 'pirate',
/* OpenNIC Alternatives */
'bazar', 'bit', 'coin', 'emc', 'fur', 'ku', 'lib', 'te', 'ti', 'uu'
];
}
if (self.ifMindExceptions && self.exceptions) { if (self.ifMindExceptions && self.exceptions) {
self.included = []; self.included = [];
self.excluded = []; self.excluded = [];
@ -175,7 +214,7 @@
}); });
if (self.included.length && !self.filteredCustomsString) { if (self.included.length && !self.filteredCustomsString) {
return [null, self, new TypeError( return [null, self, new TypeError(
'Имеются сайты, добавленные вручную. Они проксироваться не будут, т.к. нет СВОИХ проски, удовлетворяющих вашим запросам!' 'Имеются сайты, добавленные вручную. Они проксироваться не будут, т.к. нет СВОИХ проски, удовлетворяющих вашим требованиям! Если прокси всё же имеются, то проверьте требования (модификаторы).'
)]; )];
} }
} }
@ -191,69 +230,106 @@
cook(pacData, pacMods = mandatory()) { cook(pacData, pacMods = mandatory()) {
return pacMods.ifNoMods ? pacData : pacData + `${ kitchenStartsMark } return pacMods.ifNoMods ? pacData : pacData + `${ kitchenStartsMark }
;+function(global) { /******/
"use strict"; /******/;+function(global) {
/******/ "use strict";
const originalFindProxyForURL = FindProxyForURL; /******/
global.FindProxyForURL = function(url, host) { /******/ const originalFindProxyForURL = FindProxyForURL;
${function() { /******/ global.FindProxyForURL = function(url, host) {
/******/
${
function() {
let res = pacMods.ifProhibitDns ? ` let res = pacMods.ifProhibitDns ? `
global.dnsResolve = function(host) { return null; }; /******/
` : ''; /******/ global.dnsResolve = function(host) { return null; };
/******/
/******/` : '';
if (pacMods.ifProxyHttpsUrlsOnly) { if (pacMods.ifProxyHttpsUrlsOnly) {
res += ` res += `
if (!url.startsWith("https")) { /******/
return "DIRECT"; /******/ if (!url.startsWith("https")) {
/******/ return "DIRECT";
/******/ }
/******/
/******/ `;
} }
`; if (pacMods.ifUseLocalTor) {
res += `
/******/
/******/ if (host.endsWith(".onion")) {
/******/ return "${pacMods.torPoints.join('; ')}";
/******/ }
/******/
/******/ `;
} }
res += ` res += `
const directIfAllowed = ${pacMods.ifProxyOrDie ? '""/* Not allowed. */' : '"; DIRECT"'}; /******/
`; /******/ const directIfAllowed = ${pacMods.ifProxyOrDie ? '""/* Not allowed. */' : '"; DIRECT"'};
/******/`;
if (pacMods.filteredCustomsString) {
res += `
/******/
/******/ const filteredCustomProxies = "; ${pacMods.filteredCustomsString}";
/******/`;
}
const ifIncluded = pacMods.included && pacMods.included.length; const ifIncluded = pacMods.included && pacMods.included.length;
const ifExcluded = pacMods.excluded && pacMods.excluded.length; const ifExcluded = pacMods.excluded && pacMods.excluded.length;
const ifExceptions = ifIncluded || ifExcluded; const ifManualExceptions = ifIncluded || ifExcluded;
const finalExceptions = {};
if (pacMods.ifProxyMoreDomains) {
pacMods.moreDomains.reduce((acc, tld) => {
acc[tld] = true;
return acc;
}, finalExceptions);
}
if (pacMods.ifMindExceptions) {
Object.assign(finalExceptions, (pacMods.exceptions || {}));
}
const ifExceptions = Object.keys(finalExceptions).length;
if (ifExceptions) { if (ifExceptions) {
res += ` res += `
/* EXCEPTIONS START */ /******/
const dotHost = '.' + host; /******/ /* EXCEPTIONS START */
const isHostInDomain = (domain) => dotHost.endsWith('.' + domain); /******/ const dotHost = '.' + host;
const domainReducer = (maxWeight, [domain, ifIncluded]) => { /******/ const isHostInDomain = (domain) => dotHost.endsWith('.' + domain);
/******/ const domainReducer = (maxWeight, [domain, ifIncluded]) => {
if (!isHostInDomain(domain)) { /******/
return maxWeight; /******/ if (!isHostInDomain(domain)) {
} /******/ return maxWeight;
const newWeightAbs = domain.length; /******/ }
if (newWeightAbs < Math.abs(maxWeight)) { /******/ const newWeightAbs = domain.length;
return maxWeight; /******/ if (newWeightAbs < Math.abs(maxWeight)) {
} /******/ return maxWeight;
return newWeightAbs*(ifIncluded ? 1 : -1); /******/ }
/******/ return newWeightAbs*(ifIncluded ? 1 : -1);
}; /******/
/******/ };
const excWeight = ${JSON.stringify(Object.entries(pacMods.exceptions))}.reduce( domainReducer, 0 ); /******/
if (excWeight !== 0) { /******/ const excWeight = ${ JSON.stringify(Object.entries(finalExceptions)) }.reduce( domainReducer, 0 );
if (excWeight > 0) { /******/ if (excWeight !== 0) {
// Always proxy it! /******/ if (excWeight < 0) {
${ pacMods.filteredCustomsString /******/ // Never proxy it!
? `return "${pacMods.filteredCustomsString}" + directIfAllowed;` /******/ return "DIRECT";
: '/* No proxies -- continue. */' /******/ }
} /******/ // Always proxy it!
} else { ${ pacMods.filteredCustomsString
// Never proxy it! ? `/******/ return filteredCustomProxies + directIfAllowed;`
return "DIRECT"; : '/******/ /* No custom proxies -- continue. */'
} }
} /******/ }
/* EXCEPTIONS END */ /******/ /* EXCEPTIONS END */
`; `;
} }
res += ` res += `
const pacProxyString = originalFindProxyForURL(url, host)${ /******/ const pacScriptProxies = originalFindProxyForURL(url, host)${
pacMods.ifProxyOrDie ? '.replace(/DIRECT/g, "")' : ' + directIfAllowed' /******/ pacMods.ifProxyOrDie ? '.replace(/DIRECT/g, "")' : ' + directIfAllowed'
};`; };`;
if( if(
!pacMods.ifUseSecureProxiesOnly && !pacMods.ifUseSecureProxiesOnly &&
@ -261,35 +337,33 @@
pacMods.ifUsePacScriptProxies pacMods.ifUsePacScriptProxies
) { ) {
return res + ` return res + `
return pacProxyString;`; /******/ return pacScriptProxies + directIfAllowed;`;
} }
return res + ` return res + `
let pacProxyArray = pacProxyString.split(/(?:\\s*;\\s*)+/g).filter( (p) => p ); /******/ let pacProxyArray = pacScriptProxies.split(/(?:\\s*;\\s*)+/g).filter( (p) => p );
const ifNoProxies = pacProxyArray${pacMods.ifProxyOrDie ? '.length === 0' : '.every( (p) => /^DIRECT$/i.test(p) )'}; /******/ const ifNoProxies = pacProxyArray${pacMods.ifProxyOrDie ? '.length === 0' : '.every( (p) => /^DIRECT$/i.test(p) )'};
if (ifNoProxies) { /******/ if (ifNoProxies) {
// Directs only or null, no proxies. /******/ // Directs only or null, no proxies.
return "DIRECT"; /******/ return "DIRECT";
} /******/ }
return ` + /******/ return ` +
function() { function() {
if (!pacMods.ifUsePacScriptProxies) { if (!pacMods.ifUsePacScriptProxies) {
return `"${pacMods.filteredCustomsString}"`; return '';
} }
let filteredPacExp = 'pacProxyString'; let filteredPacExp = 'pacScriptProxies';
if (pacMods.ifUseSecureProxiesOnly) { if (pacMods.ifUseSecureProxiesOnly) {
filteredPacExp = filteredPacExp =
'pacProxyArray.filter( (pStr) => /^HTTPS\\s/.test(pStr) ).join("; ")'; 'pacProxyArray.filter( (pStr) => /^HTTPS\\s/.test(pStr) ).join("; ")';
} }
if ( !pacMods.filteredCustomsString ) { return filteredPacExp + ' + ';
return filteredPacExp;
}() + `${pacMods.filteredCustomsString ? 'filteredCustomProxies + ' : ''}directIfAllowed;`; // Without DIRECT you will get 'PROXY CONN FAILED' pac-error.
}()
} }
return `${filteredPacExp} + "; ${pacMods.filteredCustomsString}"`;
}() + ' + directIfAllowed;'; // Without DIRECT you will get 'PROXY CONN FAILED' pac-error.
}()}
}; };
@ -360,15 +434,12 @@
const oldProxies = getCurrentConfigs().filteredCustomsString || ''; const oldProxies = getCurrentConfigs().filteredCustomsString || '';
const newProxies = pacMods.filteredCustomsString || ''; const newProxies = pacMods.filteredCustomsString || '';
ifProxiesChanged = oldProxies !== newProxies; ifProxiesChanged = oldProxies !== newProxies;
console.log('Proxies changed from:', oldProxies, 'to', newProxies);
kitchenState(modsKey, pacMods); kitchenState(modsKey, pacMods);
} }
console.log('Keep cooked now...', pacMods);
this.setNowAsync( this.setNowAsync(
(err, res, ...setWarns) => { (err, res, ...setWarns) => {
const accWarns = modsWarns.concat(setWarns); // Acc = accumulated. const accWarns = modsWarns.concat(setWarns); // Acc = accumulated.
console.log('Try now err:', err);
if (err) { if (err) {
return cb(err, res, ...accWarns); return cb(err, res, ...accWarns);
} }

View File

@ -169,19 +169,19 @@
pacProviders: { pacProviders: {
Антизапрет: { Антизапрет: {
label: 'Антизапрет', label: 'Антизапрет',
desc: 'Альтернативный PAC-скрипт от стороннего разработчика.' + desc: `Альтернативный PAC-скрипт от стороннего разработчика.
' Блокировка определяется по доменному имени,' + Работает быстрее, но охватывает меньше сайтов.
' для некоторых провайдеров есть автоопредление.' + Блокировка определяется по доменному имени,
' <br/> <a href="https://antizapret.prostovpn.org">Страница проекта</a>.', <br/> <a href="https://antizapret.prostovpn.org">Страница проекта</a>.`,
order: 0, order: 0,
pacUrls: ['https://antizapret.prostovpn.org/proxy.pac'], pacUrls: ['https://antizapret.prostovpn.org/proxy.pac'],
}, },
Антицензорити: { Антицензорити: {
label: 'Антицензорити (<a href="https://github.com/anticensorship-russia/chromium-extension/issues/6" style="color: red">тормозит</a>)', label: 'Антицензорити',
desc: 'Основной PAC-скрипт от автора расширения.' + desc: `Основной PAC-скрипт от автора расширения.
' Блокировка определятся по доменному имени или IP адресу.' + Работает медленней, но охватывает больше сайтов.
' Работает на switch-ах. <br/>' + Блокировка определятся по доменному имени или IP адресу.<br/>
' <a href="https://rebrand.ly/ac-anticensority">Страница проекта</a>.', <a href="https://rebrand.ly/ac-anticensority">Страница проекта</a>.`,
order: 1, order: 1,
/* /*
@ -193,13 +193,16 @@
// First official, shortened: // First official, shortened:
'https://rebrand.ly/ac-chrome-anticensority-pac', 'https://rebrand.ly/ac-chrome-anticensority-pac',
// Second official, Cloud Flare with caching: // Second official, Cloud Flare with caching:
'https://anticensority.tk/generated-pac-scripts/anticensority.pac',
// GitHub.io (anticensority):
'\x68\x74\x74\x70\x73\x3a\x2f\x2f\x61\x6e\x74\x69\x63\x65\x6e\x73\x6f\x72\x69\x74\x79\x2e\x67\x69\x74\x68\x75\x62\x2e\x69\x6f\x2f\x67\x65\x6e\x65\x72\x61\x74\x65\x64\x2d\x70\x61\x63\x2d\x73\x63\x72\x69\x70\x74\x73\x2f\x61\x6e\x74\x69\x63\x65\x6e\x73\x6f\x72\x69\x74\x79\x2e\x70\x61\x63',
// GitHub repo (anticensority):
'\x68\x74\x74\x70\x73\x3a\x2f\x2f\x72\x61\x77\x2e\x67\x69\x74\x68\x75\x62\x75\x73\x65\x72\x63\x6f\x6e\x74\x65\x6e\x74\x2e\x63\x6f\x6d\x2f\x61\x6e\x74\x69\x63\x65\x6e\x73\x6f\x72\x69\x74\x79\x2f\x67\x65\x6e\x65\x72\x61\x74\x65\x64\x2d\x70\x61\x63\x2d\x73\x63\x72\x69\x70\x74\x73\x2f\x6d\x61\x73\x74\x65\x72\x2f\x61\x6e\x74\x69\x63\x65\x6e\x73\x6f\x72\x69\x74\x79\x2e\x70\x61\x63',
// Old, deprecated:
'https://anticensorship-russia.tk/generated-pac-scripts/anticensority.pac', 'https://anticensorship-russia.tk/generated-pac-scripts/anticensority.pac',
// GitHub.io: // Google Drive (0.17, anticensority):
'\x68\x74\x74\x70\x73\x3a\x2f\x2f\x61\x6e\x74\x69\x63\x65\x6e\x73\x6f\x72\x73\x68\x69\x70\x2d\x72\x75\x73\x73\x69\x61\x2e\x67\x69\x74\x68\x75\x62\x2e\x69\x6f\x2f\x67\x65\x6e\x65\x72\x61\x74\x65\x64\x2d\x70\x61\x63\x2d\x73\x63\x72\x69\x70\x74\x73\x2f\x61\x6e\x74\x69\x63\x65\x6e\x73\x6f\x72\x69\x74\x79\x2e\x70\x61\x63', // eslint-disable-line max-len '\x68\x74\x74\x70\x73\x3a\x2f\x2f\x64\x72\x69\x76\x65\x2e\x67\x6f\x6f\x67\x6c\x65\x2e\x63\x6f\x6d\x2f\x75\x63\x3f\x65\x78\x70\x6f\x72\x74\x3d\x64\x6f\x77\x6e\x6c\x6f\x61\x64\x26\x69\x64\x3d\x30\x42\x32\x6d\x68\x42\x67\x46\x6e\x66\x34\x70\x45\x4c\x56\x6c\x47\x4e\x54\x42\x45\x4d\x58\x4e\x6d\x52\x58\x63',
// GitHub repo: ],
'\x68\x74\x74\x70\x73\x3a\x2f\x2f\x72\x61\x77\x2e\x67\x69\x74\x68\x75\x62\x75\x73\x65\x72\x63\x6f\x6e\x74\x65\x6e\x74\x2e\x63\x6f\x6d\x2f\x61\x6e\x74\x69\x63\x65\x6e\x73\x6f\x72\x73\x68\x69\x70\x2d\x72\x75\x73\x73\x69\x61\x2f\x67\x65\x6e\x65\x72\x61\x74\x65\x64\x2d\x70\x61\x63\x2d\x73\x63\x72\x69\x70\x74\x73\x2f\x6d\x61\x73\x74\x65\x72\x2f\x61\x6e\x74\x69\x63\x65\x6e\x73\x6f\x72\x69\x74\x79\x2e\x70\x61\x63', // eslint-disable-line max-len
// Google Drive (0.17):
'\x68\x74\x74\x70\x73\x3a\x2f\x2f\x64\x72\x69\x76\x65\x2e\x67\x6f\x6f\x67\x6c\x65\x2e\x63\x6f\x6d\x2f\x75\x63\x3f\x65\x78\x70\x6f\x72\x74\x3d\x64\x6f\x77\x6e\x6c\x6f\x61\x64\x26\x69\x64\x3d\x30\x42\x2d\x5a\x43\x56\x53\x76\x75\x4e\x57\x66\x30\x54\x44\x46\x52\x4f\x47\x35\x46\x62\x55\x39\x4f\x64\x44\x67'], // eslint-disable-line max-len
}, },
onlyOwnSites: { onlyOwnSites: {
label: 'Только свои сайты и свои прокси', label: 'Только свои сайты и свои прокси',
@ -213,7 +216,7 @@
getSortedEntriesForProviders() { getSortedEntriesForProviders() {
return Object.entries(this.pacProviders).sort((entryA, entryB) => entryA[1].order - entryB[1].order); return Object.entries(this.pacProviders).sort((entryA, entryB) => entryA[1].order - entryB[1].order).map(([key, prov]) => Object.assign({key: key}, prov));
}, },
@ -225,6 +228,16 @@
ifFirstInstall: false, ifFirstInstall: false,
lastPacUpdateStamp: 0, lastPacUpdateStamp: 0,
setTitle() {
const upDate = new Date(this.lastPacUpdateStamp).toLocaleString('ru-RU')
.replace(/:\d+$/, '').replace(/\.\d{4}/, '');
chrome.browserAction.setTitle({
title: `Обновлялись ${upDate} | Версия ${window.apis.version.build}`,
});
},
_currentPacProviderLastModified: 0, // Not initialized. _currentPacProviderLastModified: 0, // Not initialized.
getLastModifiedForKey(key = mandatory()) { getLastModifiedForKey(key = mandatory()) {
@ -331,6 +344,7 @@
this.lastPacUpdateStamp = Date.now(); this.lastPacUpdateStamp = Date.now();
this.ifFirstInstall = false; this.ifFirstInstall = false;
this.setAlarms(); this.setAlarms();
this.setTitle();
} }
resolve([err, null, ...warns]); resolve([err, null, ...warns]);
@ -509,10 +523,10 @@
2. We have to check storage for migration before using it. 2. We have to check storage for migration before using it.
Better on each launch then on each pull. Better on each launch then on each pull.
*/ */
const ifUpdating = antiCensorRu.version !== oldStorage.version;
await new Promise((resolve) => { await new Promise((resolve) => {
const ifUpdating = antiCensorRu.version !== oldStorage.version;
if (!ifUpdating) { if (!ifUpdating) {
// LAUNCH, RELOAD, ENABLE // LAUNCH, RELOAD, ENABLE
@ -527,12 +541,7 @@
const key = antiCensorRu._currentPacProviderKey; const key = antiCensorRu._currentPacProviderKey;
if (key !== null) { if (key !== null) {
const ifVeryOld = !Object.keys(antiCensorRu.pacProviders).includes(key); const ifVeryOld = !Object.keys(antiCensorRu.pacProviders).includes(key);
const ifNeedsForcing = (oldStorage.version < '0.0.0.2') && !localStorage.getItem('provider-backup'); if (ifVeryOld) {
if ( ifVeryOld || ifNeedsForcing ) {
if (ifNeedsForcing) {
console.log('Update forces antizapret...')
localStorage.setItem('provider-backup', antiCensorRu._currentPacProviderKey);
}
antiCensorRu._currentPacProviderKey = 'Антизапрет'; antiCensorRu._currentPacProviderKey = 'Антизапрет';
} }
} }
@ -549,6 +558,7 @@
if (antiCensorRu.getPacProvider()) { if (antiCensorRu.getPacProvider()) {
antiCensorRu.setAlarms(); antiCensorRu.setAlarms();
} }
antiCensorRu.setTitle();
/* /*
History of Changes to Storage (Migration Guide) History of Changes to Storage (Migration Guide)

View File

@ -0,0 +1,63 @@
'use strict';
{
window.apis.menus = {
getItemsAsObject: () => ({
googleTranslate: {
title: 'Через Google Translate',
getUrl: (blockedUrl) => (
'https://translate.google.com/translate?hl=&sl=en&tl=ru&anno=2&sandbox=1&u=' + blockedUrl),
order: 0,
},
hostTracker: {
title: 'Из кэша Google',
getUrl: (blockedUrl) => 'http://webcache.googleusercontent.com/search?q=cache:' + blockedUrl,
order: 1,
},
archiveOrg: {
title: 'Из архива archive.org',
getUrl: (blockedUrl) => 'https://web.archive.org/web/*/' + blockedUrl,
order: 2,
},
otherUnblock: {
title: 'Разблокировать по-другому',
getUrl: (blockedUrl) => ('https://rebrand.ly/ac-unblock#' + blockedUrl),
order: 3,
},
antizapretInfo: {
title: 'Сайт в реестре блокировок?',
getUrl: (blockedUrl) => 'https://antizapret.info/index.php?search=' + new URL(blockedUrl).hostname,
order: 4,
},
support: {
title: 'Документация / Помощь / Поддержка',
getUrl: (blockedUrl) => 'https://rebrand.ly/ac-wiki',
order: 99,
},
}),
getItemsAsArray: function() {
const itemsObj = this.getItemsAsObject();
return Object.keys(itemsObj).reduce((acc, key) => {
acc.push(itemsObj[key]);
return acc;
}, [])
.sort((a, b) => a.order - b.order);
},
};
}

View File

@ -0,0 +1,45 @@
'use strict';
{
const chromified = window.utils.chromified;
let seqId = 0;
const createMenuLinkEntry = (title, tab2url) => {
const id = (++seqId).toString();
chrome.contextMenus.create({
id: id,
title: title,
contexts: ['browser_action'],
}, chromified((err) => {
if(err) {
console.warn('Context menu error ignored:', err);
}
}));
chrome.contextMenus.onClicked.addListener((info, tab) => {
if(info.menuItemId === id) {
Promise.resolve( tab2url( tab ) )
.then( (url) => chrome.tabs.create({url: url}) );
}
});
};
window.apis.menus.getItemsAsArray().forEach((item) => {
createMenuLinkEntry(
item.title,
(tab) => item.getUrl(tab.url)
);
});
}

View File

@ -1,72 +0,0 @@
'use strict';
{
const chromified = window.utils.chromified;
let seqId = 0;
const createMenuLinkEntry = (title, tab2url) => {
const id = (++seqId).toString();
chrome.contextMenus.create({
id: id,
title: title,
contexts: ['browser_action'],
}, chromified((err) => {
if(err) {
console.warn('Context menu error ignored:', err);
}
}));
chrome.contextMenus.onClicked.addListener((info, tab) => {
if(info.menuItemId === id) {
Promise.resolve( tab2url( tab ) )
.then( (url) => chrome.tabs.create({url: url}) );
}
});
};
createMenuLinkEntry(
'Сайт доступен из-за границы? Is up?',
(tab) => `data:text/html;charset=utf8,<title>Запрашиваю...</title>
<form class='tracker-form' method='POST'
action='https://www.host-tracker.com/ru/InstantCheck/Create'>
<input name='InstantCheckUrl' value='${new URL(tab.url).hostname}'
type='hidden'>
</form>
<script>document.querySelector('.tracker-form').submit()<\/script>`
);
createMenuLinkEntry(
'Сайт в реестре блокировок?',
(tab) => 'https://antizapret.info/index.php?search=' + new URL(tab.url).hostname
);
createMenuLinkEntry(
'Из архива archive.org',
(tab) => 'https://web.archive.org/web/*/' + tab.url
);
createMenuLinkEntry(
'Через Google Translate',
(tab) => 'https://translate.google.com/translate?hl=&sl=en&tl=ru&anno=2&sandbox=1&u=' + tab.url
);
createMenuLinkEntry(
'Разблокировать по-другому',
(tab) => 'https://rebrand.ly/ac-unblock#' + tab.url
);
createMenuLinkEntry(
'Документация / Помощь / Поддержка',
(tab) => 'https://rebrand.ly/ac-support'
);
}

View File

@ -32,16 +32,17 @@
${scripts_2x} ${scripts_2x}
, "35-pac-kitchen-api.js" , "35-pac-kitchen-api.js"
, "37-sync-pac-script-with-pac-provider-api.js" , "37-sync-pac-script-with-pac-provider-api.js"
${scripts_7x} ${scripts_8x}
, "80-context-menus.js" , "70-menu-items.js"
, "75-context-menus.js"
] ]
}, },
"browser_action": { "browser_action": {
"default_title": "Этот сайт благословлён | Версия ${version + versionSuffix}", "default_title": "Этот сайт благословлён | Версия ${version + versionSuffix}",
"default_popup": "/pages/choose-pac-provider/index.html" "default_popup": "/pages/options/index.html"
}, },
"options_ui": { "options_ui": {
"page": "/pages/choose-pac-provider/index.html", "page": "/pages/options/index.html",
"chrome_style": true "chrome_style": false
} }
} }

View File

@ -1,562 +0,0 @@
<!DOCTYPE html>
<html style="display: none; will-change: contents, display">
<head>
<meta charset="utf-8">
<title>Настройки</title>
<style>
:root {
--ribbon-color: #4169e1;
--blue-bg: dodgerblue;
--default-grey: #bfbfbf;
max-width: 28em;
}
body {
margin: 0;
}
a, a:visited {
color: var(--ribbon-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
label {
user-select: none;
}
div, section, header, ul, ol {
margin: 0;
padding: 0;
}
header {
margin-bottom: 0.3em
}
ul, ol {
list-style-type: none;
}
:root:not(.if-options-page) ul,
:root:not(.if-options-page) ol {
/*Here is a flex bug:
() antizapret [update] (i)
() anticensority very_long_foobar [update] (i) <- Sic!
Also: options page is wider, check it too.
But: fixed 100% width conflicts with margins/paddings.
So: use only when needed and avoid margins.
FYI: setting left-margin fixes problem too, but margins are not wanted.
Fix this problem below:
*/
display: inline-block;
min-width: 100%;
}
:root.if-options-page ul,
:root.if-options-page ol,
#list-of-notifiers {
margin-left: 0.4em;
}
li, footer {
display: block;
white-space: nowrap;
word-break: keep-all;
}
li, li > * {
vertical-align: middle;
}
input[type="radio"], input[type="checkbox"] {
flex-shrink: 0;
}
input[type="radio"], label {
cursor: pointer;
}
hr {
border-width: 1px 0 0 0;
margin: 0 0 0.6em 0;
padding: 0;
}
em {
font-style: normal;
text-decoration: underline;
}
/* COMMON 1 */
.hor-padded {
padding-left: 1.4em;
padding-right: 1.4em;
}
.horizontal-list li {
display: inline-block;
}
/* NOT CONTROLLED */
.if-not-controlled {
display: none;
background-color: red;
color: white;
font-weight: bold;
text-align: center;
padding-top: 1em;
padding-bottom: 1em;
border-bottom: 1px solid var(--default-grey);
}
:root.if-options-page .if-not-controlled {
padding-top: 0;
padding-bottom: 0;
}
.if-not-controlled a {
color: white;
}
/* MINI VS FULL */
:root:not(.if-version-mini) .only-for-mini-version {
display: none;
}
:root.if-version-mini .only-for-full-version {
display: none;
}
/* OPTIONS PAGE */
:root:not(.if-options-page) .only-for-options-page {
display: none;
}
:root.if-options-page .hidden-for-options-page {
display: none;
}
/* ACCORDION (OR TABBED STATEFUL UI) */
.off {
display: none;
}
section[data-for] {
padding: 0.6em 0 1em;
}
:root.if-options-page section[data-for] {
padding-bottom: 0.6em;
}
:root.if-options-page section[data-for]:not(:last-child) {
border-bottom: 1px solid var(--default-grey);
}
/* HIDE */
:root:not(.if-options-page) #acc-pac:not(:checked) ~ .main-nav section[data-for="acc-pac"],
:root:not(.if-options-page) #acc-exc:not(:checked) ~ .main-nav section[data-for="acc-exc"],
:root:not(.if-options-page) #acc-mods:not(:checked) ~ .main-nav section[data-for="acc-mods"],
:root:not(.if-options-page) #acc-ntf:not(:checked) ~ .main-nav section[data-for="acc-ntf"]
{
/* Hide, but preclude width resizes. */
height: 0px !important;
line-height: 0px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
border: none !important;
display: block;
visibility: hidden;
transform: scaleY(0) !important;
}
:root:not(.if-options-page) #acc-pac:not(:checked) ~ .main-nav section[data-for="acc-pac"] *,
:root:not(.if-options-page) #acc-exc:not(:checked) ~ .main-nav section[data-for="acc-exc"] *,
:root:not(.if-options-page) #acc-mods:not(:checked) ~ .main-nav section[data-for="acc-mods"] *,
:root:not(.if-options-page) #acc-ntf:not(:checked) ~ .main-nav section[data-for="acc-ntf"] *
{
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.nav-labels {
text-align: center;
}
.nav-labels li label {
display: inline-block;
border: 2px solid var(--blue-bg);
border-radius: 0.7em;
background-color: white;
color: var(--ribbon-color);
padding: 0.2em 0.65em 0.3em 0.4em;
line-height: 0.9em;
margin: 0.1em 0;
}
.nav-labels li label:hover {
background-color: var(--blue-bg);
color: white;
border-color: white;
border-style: dotted;
}
/* CHECKED LABELS */
#acc-pac:checked ~ .nav-labels label[for="acc-pac"]:not(:hover),
#acc-exc:checked ~ .nav-labels label[for="acc-exc"]:not(:hover),
#acc-mods:checked ~ .nav-labels label[for="acc-mods"]:not(:hover),
#acc-ntf:checked ~ .nav-labels label[for="acc-ntf"]:not(:hover)
{
background-color: var(--blue-bg);
color: white;
line-height: 0.8em;
}
/* ★★★★★ */
.nav-labels label:before {
content: '★ ';
visibility: hidden;
}
.nav-labels li label:hover:before,
#acc-pac:checked ~ .nav-labels label[for="acc-pac"]:before,
#acc-exc:checked ~ .nav-labels label[for="acc-exc"]:before,
#acc-mods:checked ~ .nav-labels label[for="acc-mods"]:before,
#acc-ntf:checked ~ .nav-labels label[for="acc-ntf"]:before
{
visibility: initial;
}
/* COMMON 2 */
/* INFO SIGNS */
.info-row {
position: relative;
}
.right-bottom-icon {
margin-left: 0.1em;
vertical-align: bottom;
}
.info-url, .info-url:hover {
text-decoration: none;
}
/* Source: https://jsfiddle.net/greypants/zgCb7/ */
.desc {
text-align: right;
color: var(--ribbon-color);
cursor: help;
padding-left: 0.3em;
}
.tooltip {
display: none;
position: absolute;
white-space: initial;
word-break: initial;
top: 100%;
left: 0;
right: 1em;
z-index: 1;
background-color: var(--ribbon-color);
padding: 1em;
color: white;
text-align: initial;
}
.desc:hover .tooltip {
display: block;
}
.tooltip a,
.tooltip em {
color: white;
}
.desc .tooltip:after {
border-left: solid transparent 0.5em;
border-bottom: solid var(--ribbon-color) 0.5em;
position: absolute;
top: -0.5em;
content: "";
width: 0;
right: 0;
height: 0;
}
/* This bridges the gap so you can mouse into the tooltip without it disappearing. */
.desc .tooltip:before {
position: absolute;
top: -1em;
content: "";
display: block;
height: 1.2em;
left: 75%;
width: calc(25% + 0.6em);
}
/* TAB_1 PAC PROVIDER */
.update-button {
visibility: hidden;
}
input:checked ~ .label-container .update-button {
visibility: inherit;
}
label[for="onlyOwnSites"] + .update-button {
display: none;
}
#none:checked ~ .label-container label {
color: red;
}
#update-message {
white-space: nowrap;
margin-top: 0.5em;
}
/* TAB_2 PAC MODS */
#pac-mods label {
display: block;
}
#pac-mods label:first-letter {
text-transform: uppercase;
}
#mods-custom-proxy-string-raw ~ textarea {
width: 100%;
height: 7em;
margin-top: 0.3em;
font-size: 0.9em;
}
#mods-custom-proxy-string-raw:not(:checked) ~ textarea {
display: none;
}
/* TAB_3 EXCEPTIONS */
#exc-address-container {
display: flex;
align-items: center;
width: 100%;
}
#exc-address-container > a {
border-bottom: 1px solid transparent;
margin-left: 0.2em;
}
#exc-address {
width: 100%;
display: flex;
align-items: baseline;
--exc-hieght: 1.6em;
font-size: 1em;
border-bottom: 1px solid var(--ribbon-color) !important;
}
input#exc-editor {
border: none;
width: 100%;
background: inherit;
/* The two below align '.' (dot) vertically. */
max-height: var(--exc-hieght) !important;
min-height: var(--exc-hieght) !important;
}
#exc-radio {
display: flex;
justify-content: space-around;
margin-top: 0.5em;
}
[name="if-proxy-this-site"]:checked + label {
font-weight: bold;
}
#exc-address.if-yes {
background-color: lightgreen;
}
#exc-address.if-no {
background-color: pink;
}
option.if-proxied {
color: var(--ribbon-color);
}
option:not(.if-proxied) {
color: red;
}
/* CONTROL RAW = BUTTON + LINK */
.hor-flex {
display: flex;
align-items: baseline;
justify-content: space-between;
width: 100%;
}
.control-row {
margin: 1em 0 1em 0;
}
.hor-flex > input:not([type="button"]) {
align-self: flex-end;
}
.label-container {
flex-grow: 9;
padding-left: 0.3em;
}
/* STATUS */
#status-row {
padding: 0 0.3em 1em;
}
#status {
display: inline-block;
}
.other-version {
font-size: 1.7em;
color: var(--ribbon-color);
margin-left: 0.1em;
}
.other-version:hover {
text-decoration: none;
}
.full-line-height,
.full-line-height * {
line-height: 100%;
}
@font-face {
font-family: "emoji";
src:url("../lib/fonts/emoji.woff") format("woff");
font-weight: normal;
font-style: normal;
}
.emoji {
font-family: "emoji";
}
svg.icon {
display: inline-block;
width: 1em;
height: 1em;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
}
</style>
</head>
<body>
<section class="if-not-controlled hor-padded" id="which-extension"></section>
<input type="radio" name="accordion" class="off" id="acc-pac" checked/>
<input type="radio" name="accordion" class="off" id="acc-exc"/>
<input type="radio" name="accordion" class="off" id="acc-mods"/>
<input type="radio" name="accordion" class="off" id="acc-ntf"/>
<nav class="nav-labels horizontal-list hidden-for-options-page">
<ul>
<li><label for="acc-pac" class="nav-label">PAC-скрипт</label></li>
<li><label for="acc-mods" class="nav-label">Модификаторы</label></li>
<li><label for="acc-exc" class="nav-label">Исключения</label></li>
<li><label for="acc-ntf" class="nav-label">Уведомления</label></li>
</ul>
<hr/>
</nav>
<nav class="hor-padded main-nav">
<section data-for="acc-pac">
<header class="only-for-options-page">PAC-скрипт:</header>
<ul id="list-of-providers">
<li class="info-row hor-flex"><input type="radio" name="pacProvider" id="none" checked> <div class="label-container"><label for="none">Отключить</label></div></li>
</ul>
<div id="update-message" class="hor-flex" style="align-items: center">
<div>Обновлялись: <span class="update-date">...</span></div>
<div class="full-line-height">
<a class="only-for-mini-version other-version emoji" href="https://rebrand.ly/ac-versions"
title="Полная версия">🏋</a>
<a class="only-for-full-version other-version emoji" href="https://rebrand.ly/ac-versions"
title="Версия для слабых машин">🐌</a>
</div>
</div>
</section>
<section data-for="acc-exc" class="only-for-options-page">
Редактор исключений доступен толко для <a href="chrome://newtab">вкладок</a>.
</section>
<section data-for="acc-exc" class="hidden-for-options-page">
<div>Проксировать указанный сайт?</div>
<div id="exc-address-container">
<div id="exc-address">
<span>*.</span><input placeholder="navalny.com" list="exc-list" name="browser" id="exc-editor" style=""/>
</div>
<a href="../exceptions/index.html" title="импорт/экспорт"><svg
class="icon"
><use xlink:href="#icon-import-export"></use></svg>
</a>
</div>
<datalist id="exc-list"></datalist>
<ol class="horizontal-list" id="exc-radio">
<li><input id="this-auto" type="radio" checked name="if-proxy-this-site"/>
<label for="this-auto"><!--span class="emoji">🔄(looks fat)</span--><svg
class="icon"
style="position: relative; top: 0.15em;"><use xlink:href="#icon-loop-round"></use></svg>&nbsp;авто</label>
</li>
<li><input id="this-yes" type="radio" name="if-proxy-this-site"/> <label for="this-yes">&nbsp;да</label></li>
<li><input id="this-no" type="radio" name="if-proxy-this-site"/> <label for="this-no">&nbsp;нет</label></li>
</ol>
</section>
<section data-for="acc-mods">
<ul id="pac-mods">
<li class="control-row hor-flex">
<input type="button" value="Применить" id="apply-mods" disabled/>
<a href id="reset-mods">К изначальным!</a>
</li>
</ul>
</section>
<section data-for="acc-ntf">
<header>Я <span style="color: #f93a17"></span>едомления:</header>
<ul id="list-of-notifiers"></ul>
</section>
</nav>
<hr/>
<div class="hor-padded">
<section id="status-row">
<div id="status" style="will-change: contents">Загрузка...</div>
</section>
<footer class="control-row hor-flex">
<input type="button" value="Готово" class="close-button">
<a href="../troubleshoot/index.html">
Проблемы?
</a>
</footer>
</div>
<script src="./index.js"></script>
<script src="../lib/keep-links-clickable.js"></script>
<!-- ICONS -->
<svg style="display: none" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<symbol id="icon-info" viewBox="0 0 100 100">
<title>info</title>
<circle shape-rendering="geometricPrecision" fill="none" stroke="currentColor" stroke-width="7" cx="50" cy="50" r="45"/>
<path shape-rendering="crispEdges" d="M 55,40 V 80 H 45 V 40 z m 0,-20 V 35 H 45 V 20 Z"/>
</symbol>
<symbol id="icon-loop-round" viewBox="0 0 32 32">
<title>loop-round</title>
<path d="M27.802 5.197c-2.925-3.194-7.13-5.197-11.803-5.197-8.837 0-16 7.163-16 16h3c0-7.18 5.82-13 13-13 3.844 0 7.298 1.669 9.678 4.322l-4.678 4.678h11v-11l-4.198 4.197z"/>
<path d="M29 16c0 7.18-5.82 13-13 13-3.844 0-7.298-1.669-9.678-4.322l4.678-4.678h-11v11l4.197-4.197c2.925 3.194 7.13 5.197 11.803 5.197 8.837 0 16-7.163 16-16h-3z"/>
</symbol>
<symbol id="icon-import-export" viewBox="0 0 32 32">
<title>import-export</title>
<g transform="rotate(0 16 16)">
<path d="M7 22 h 25 v 4 h -25 v 5 l -7-7 7-7 v5 z"/>
<path d="M25 10 h-25 v-4 h 25 v -5 l 7 7 -7 7 z"/>
</g>
<!-- With bars on peaks.
<path d="M30 0h2v16h-2v-16z"></path>
<path d="M0 16h2v16h-2v-16z"></path>
<path d="M10 22 h 22 v 4 h -22 v 5 l -7-7 7-7 v5 z"></path>
<path d="M22 10 h-22 v-4 h 22 v -5 l 7 7 -7 7 z"></path-->
</symbol>
</svg>
</body>
</html>

View File

@ -1,684 +0,0 @@
'use strict';
const START = Date.now();
document.getElementById('pac-mods').onchange = function() {
this.classList.add('changed');
document.getElementById('apply-mods').disabled = false;
};
chrome.runtime.getBackgroundPage( (backgroundPage) =>
backgroundPage.apis.errorHandlers.installListenersOn(
window, 'PUP', async() => {
const getStatus = () => document.querySelector('#status');
const setStatusTo = (msg) => {
getStatus().innerHTML = msg || 'Хорошего настроения Вам!';
};
const antiCensorRu = backgroundPage.apis.antiCensorRu;
const errorHandlers = backgroundPage.apis.errorHandlers;
// SET DATE
const setDate = () => {
let dateForUser = 'никогда';
if( antiCensorRu.lastPacUpdateStamp ) {
let diff = Date.now() - antiCensorRu.lastPacUpdateStamp;
let units = 'мс';
const gauges = [
[1000, 'с'],
[60, 'мин'],
[60, 'ч'],
[24, 'дн'],
[7, ' недель'],
[4, ' месяцев'],
];
for(const g of gauges) {
const diffy = Math.floor(diff / g[0]);
if (!diffy)
break;
diff = diffy;
units = g[1];
}
dateForUser = diff + units + ' назад';
}
const dateElement = document.querySelector('.update-date');
dateElement.innerText = dateForUser + ' / ' +
(antiCensorRu.pacUpdatePeriodInMinutes/60) + 'ч';
dateElement.title = new Date(antiCensorRu.lastPacUpdateStamp)
.toLocaleString('ru-RU');
};
setDate();
chrome.storage.onChanged.addListener(
(changes) => changes.lastPacUpdateStamp.newValue && setDate()
);
// CLOSE BUTTON
document.querySelector('.close-button').onclick = () => window.close();
// RADIOS FOR PROVIDERS
const currentProviderRadio = () => {
const id = antiCensorRu.getCurrentPacProviderKey() || 'none';
return document.getElementById(id);
};
const checkChosenProvider = () => {
currentProviderRadio().checked = true;
};
const showErrors = (err, ...warns) => {
const warning = warns
.map(
(w) => w && w.message || ''
)
.filter( (m) => m )
.map( (m) => '✘ ' + m )
.join('<br/>');
let message = '';
if (err) {
let wrapped = err.wrapped;
message = err.message || '';
while( wrapped ) {
const deeperMsg = wrapped && wrapped.message;
if (deeperMsg) {
message = message + ' &gt; ' + deeperMsg;
}
wrapped = wrapped.wrapped;
}
}
message = message.trim();
if (warning) {
message = message ? message + '<br/>' + warning : warning;
}
setStatusTo(
`<span style="color:red">
${err ? '<span class="emoji">🔥</span> Ошибка!' : 'Некритичная ошибка.'}
</span>
<br/>
<span style="font-size: 0.9em; color: darkred">${message}</span>
${err ? '<a href class="link-button">[Техн.детали]</a>' : ''}`
);
if (err) {
getStatus().querySelector('.link-button').onclick = function() {
errorHandlers.viewError('pup-ext-err', err);
return false;
};
}
};
const switchInputs = function(val) {
const inputs = document.querySelectorAll('input');
for ( let i = 0; i < inputs.length; i++ ) {
inputs[i].disabled = val === 'on' ? false : true;
}
};
const conduct = (beforeStatus, operation, afterStatus, onSuccess = () => {}, onError = () => {}) => {
setStatusTo(beforeStatus);
switchInputs('off');
operation((err, res, ...warns) => {
warns = warns.filter( (w) => w );
if (err || warns.length) {
showErrors(err, ...warns);
} else {
setStatusTo(afterStatus);
}
switchInputs('on');
if (!err) {
onSuccess(res);
} else {
onError(err);
}
});
};
const infoIcon = `
<svg class="icon"
style="position: relative; top: 0.08em"><use xlink:href="#icon-info"></use></svg>
<!--span style="font-size: 1.3em" class="emoji">🛈(looks huge)</span-->
`;
const infoSign = function infoSign(tooltip) {
return `<div class="desc">
${infoIcon}
<div class="tooltip">${tooltip}</div>
</div>`;
};
{
const ul = document.querySelector('#list-of-providers');
const _firstChild = ul.firstChild;
for(
const [providerKey, provider] of antiCensorRu.getSortedEntriesForProviders()
) {
const li = document.createElement('li');
li.classList.add('info-row', 'hor-flex');
li.innerHTML = `
<input type="radio" name="pacProvider" id="${providerKey}">
<div class="label-container">
<label for="${providerKey}"> ${provider.label}</label>
&nbsp;<a href class="link-button update-button"
id="update-${providerKey}">[обновить]</a>
</div>` +
infoSign(provider.desc);
li.querySelector('.link-button').onclick =
() => {
conduct(
'Обновляем...', (cb) => antiCensorRu.syncWithPacProviderAsync(cb),
'Обновлено.'
);
return false;
};
ul.insertBefore( li, _firstChild );
}
checkChosenProvider();
}
const radios = [].slice.apply(
document.querySelectorAll('[name=pacProvider]')
);
for(const radio of radios) {
radio.onclick = function(event) {
if (
event.target.id === (
antiCensorRu.getCurrentPacProviderKey() || 'none'
)
) {
return false;
}
const pacKey = event.target.id;
if (pacKey === 'none') {
conduct(
'Отключение...',
(cb) => antiCensorRu.clearPacAsync(cb),
'Отключено.',
checkChosenProvider
);
} else {
conduct(
'Установка...',
(cb) => antiCensorRu.installPacAsync(pacKey, cb),
'PAC-скрипт установлен.',
checkChosenProvider
);
}
return false;
};
}
// IF MINI
if (backgroundPage.apis.version.ifMini) {
document.documentElement.classList.add('if-version-mini');
}
// IF INSIDE OPTIONS TAB
const currentTab = await new Promise(
(resolve) => chrome.tabs.query(
{active: true, currentWindow: true},
([tab]) => resolve(tab)
)
);
const ifInsideOptions = !currentTab || currentTab.url.startsWith('chrome://extensions/?options=');
if (ifInsideOptions) {
document.documentElement.classList.add('if-options-page');
}
// EXCEPTIONS PANEL
{
const pacKitchen = backgroundPage.apis.pacKitchen;
{
const excEditor = document.getElementById('exc-editor');
const validateHost = function validateHost(host) {
const ValidHostnameRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
if(!ValidHostnameRegex.test(host)) {
showErrors(new TypeError('Должно быть только доменное имя, без протокола, порта и пути. Попробуйте ещё раз.'));
return false;
}
return true;
};
const labelIfProxied = '✔';
const labelIfNotProxied = '✘';
const labelIfAuto = '↻';
const addOption = function addOption(host, yesNoUndefined) {
const opt = document.createElement('option');
// `value` may be changed for hiding line in the tooltip.
opt.value = host;
opt.dataset.host = host;
switch(yesNoUndefined) {
case true:
opt.label = labelIfProxied;
break;
case false:
opt.label = labelIfNotProxied;
break;
default:
opt.label = labelIfAuto;
}
const editorHost = excEditor.value.trim();
if (host === editorHost) {
excList.insertBefore( opt, excList.firstChild );
} else {
excList.appendChild(opt);
}
return opt;
};
const thisYes = document.getElementById('this-yes');
const thisNo = document.getElementById('this-no');
const thisAuto = document.getElementById('this-auto');
const yesClass = 'if-yes';
const noClass = 'if-no';
function moveCursorIfNeeded() {
const nu = excEditor.dataset.moveCursorTo;
if(nu !== undefined) {
excEditor.setSelectionRange(nu, nu);
delete excEditor.dataset.moveCursorTo;
}
}
const hideOpt = (opt) => opt.value = '\n';
const unhideOptAndAddSpace = (opt) => opt.value = opt.dataset.host + ' ';
const excList = document.getElementById('exc-list');
excEditor.onkeydown = function(event) {
moveCursorIfNeeded();
if(event.key === 'Enter') {
// Hide all.
excList.childNodes.forEach( hideOpt );
}
return true;
};
const renderExceptionsPanelFromExcList = function renderExceptionsPanelFromExcList(event) {
// If triangle button on right of datalist input clicked.
let ifTriangleClicked = false;
const ifClick = event && event.type === 'click';
{
const maxIndentFromRightInPx = 15;
ifTriangleClicked = ifClick
&& !excEditor.selectionStart && !excEditor.selectionEnd
&& event.x > excEditor.getBoundingClientRect().right - maxIndentFromRightInPx;
}
const setInputValue = (newValue) => {
if (ifClick && !ifTriangleClicked) {
// Don't jerk cursor on simple clicks.
return;
}
// See bug in my comment to http://stackoverflow.com/a/32394157/521957
// First click on empty input may be still ignored.
const nu = excEditor.selectionStart + newValue.length - excEditor.value.length;
excEditor.value = newValue;
excEditor.dataset.moveCursorTo = nu;
window.setTimeout(moveCursorIfNeeded, 0);
};
const originalHost = excEditor.value.trim();
const ifInit = !event;
const currentHost = ifTriangleClicked ? '' : (originalHost || (ifInit ? '' : ' '));
setInputValue(currentHost);
let exactOpt = false;
let editedOpt = false;
excList.childNodes.forEach(
(opt) => {
unhideOptAndAddSpace(opt);
if(opt.label === labelIfAuto) {
editedOpt = opt;
return;
}
if (opt.dataset.host === originalHost) {
exactOpt = opt;
}
}
);
thisAuto.checked = true;
excEditor.parentNode.classList.remove(noClass, yesClass);
const ifInputEmpty = !originalHost;
if (ifTriangleClicked || ifInputEmpty) {
// Show all opts.
if (editedOpt) {
// Example of editedOpt.value: 'abcde ' <- Mind the space (see unhideOptAndAddSpace)!
const ifBackspacedOneChar = ifInputEmpty && editedOpt.value.length < 3;
if (ifBackspacedOneChar) {
editedOpt.remove();
}
}
return true;
}
if (editedOpt) {
const ifEditedOptAlreadyExists = editedOpt.dataset.host === originalHost;
if(ifEditedOptAlreadyExists) {
hideOpt(editedOpt);
return true;
}
// Not exact! Update!
editedOpt.remove();
}
if (!exactOpt) {
editedOpt = addOption(originalHost, undefined);
if (!ifClick) {
// New value was typed -- don't show tooltip.
hideOpt(editedOpt);
}
return true;
}
// Exact found!
excList.childNodes.forEach(hideOpt);
if(exactOpt.label === labelIfProxied) {
thisYes.checked = true;
excEditor.parentNode.classList.add(yesClass);
} else {
thisNo.checked = true;
excEditor.parentNode.classList.add(noClass);
}
return true;
};
excEditor.onclick = excEditor.oninput = renderExceptionsPanelFromExcList;
if (currentTab && !currentTab.url.startsWith('chrome')) {
excEditor.value = new URL(currentTab.url).hostname;
} else {
// Show placeholder.
excEditor.value = '';
}
{ // Populate selector.
const pacMods = pacKitchen.getPacMods();
for(const host of Object.keys(pacMods.exceptions || {}).sort()) {
addOption(host, pacMods.exceptions[host]);
}
renderExceptionsPanelFromExcList(); // Colorize input.
}
document.getElementById('exc-radio').onclick = function(event) {
/* ON CLICK */
if(event.target.tagName !== 'INPUT') {
// Only label on checkbox.
return true;
}
const host = excEditor.value.trim();
const pacMods = pacKitchen.getPacMods();
pacMods.exceptions = pacMods.exceptions || {};
let fixOptions;
const curOptOrNull = excList.querySelector(`[data-host="${host}"]`);
if (thisAuto.checked) {
delete pacMods.exceptions[host];
fixOptions = () => {
curOptOrNull && curOptOrNull.remove();
}
} else {
// YES or NO checked.
const ifYesClicked = thisYes.checked;
if (!validateHost(host)) {
return false;
}
if (ifYesClicked && !pacMods.filteredCustomsString) {
showErrors( new TypeError(
'Проксировать СВОИ сайты можно только при наличии СВОИХ прокси (см. «Модификаторы» ). Нет своих прокси, удовлетворяющих вашим требованиям.'
));
return false;
}
//const ifNew = !(host in pacMods.exceptions);
pacMods.exceptions[host] = ifYesClicked;
// Change label.
fixOptions = () => {
if (curOptOrNull) {
curOptOrNull.label = ifYesClicked ? labelIfProxied : labelIfNotProxied;
} else {
addOption(host, ifYesClicked);
}
};
}
conduct(
'Применяем исключения...',
(cb) => pacKitchen.keepCookedNowAsync(pacMods, cb),
'Исключения применены. Не забывайте о кэше!',
() => {
fixOptions();
// Window may be closed before this line executes.
renderExceptionsPanelFromExcList();
}
);
return false; // Don't check before operation is finished.
};
}
// PAC MODS PANEL
const modPanel = document.getElementById('pac-mods');
const _firstChild = modPanel.firstChild;
const keyToLi = {};
const customProxyStringKey = 'customProxyStringRaw';
const uiRaw = 'ui-proxy-string-raw';
for(const conf of pacKitchen.getOrderedConfigs()) {
const key = conf.key;
const iddy = 'mods-' + conf.key.replace(/([A-Z])/g, (_, p) => '-' + p.toLowerCase());
const li = document.createElement('li');
li.classList.add('info-row', 'hor-flex');
keyToLi[key] = li;
const ifMultiline = key === customProxyStringKey;
li.innerHTML = `
<input type="checkbox" id="${iddy}" ${ conf.value ? 'checked' : '' }/>
<div class="label-container">
<label for="${iddy}"> ${ conf.label }</label>
</div>`;
if (!ifMultiline) {
li.innerHTML += infoSign(conf.desc);
} else {
li.style.flexWrap = 'wrap';
li.innerHTML += `<a href="${conf.url}" class="right-bottom-icon info-url">${infoIcon}</a>
<textarea
spellcheck="false"
placeholder="SOCKS5 localhost:9050; # Tor Expert
SOCKS5 localhost:9150; # Tor Browser
HTTPS 11.22.33.44:3143;
PROXY foobar.com:8080; # Not HTTP!">${conf.value || localStorage.getItem(uiRaw) || ''}</textarea>`;
li.querySelector('textarea').onkeyup = function() {
this.dispatchEvent( new Event('change', {'bubbles': true}) );
};
}
modPanel.insertBefore( li, _firstChild );
};
document.getElementById('apply-mods').onclick = () => {
const oldMods = pacKitchen.getPacMods();
for(const key of Object.keys(keyToLi)) {
oldMods[key] = keyToLi[key].querySelector('input').checked;
};
{
// OWN PROXY
const liPs = keyToLi[customProxyStringKey];
oldMods[customProxyStringKey]
= liPs.querySelector('input').checked
&& liPs.querySelector('textarea').value.trim();
const taVal = liPs.querySelector('textarea').value;
if (oldMods[customProxyStringKey] !== false) {
const ifValidArr = taVal
.trim()
.replace(/#.*$/mg, '')
.split(/\s*[;\n\r]+\s*/g)
.filter( (str) => str );
const ifValid = ifValidArr.every(
(str) =>
/^(?:DIRECT|(?:(?:HTTPS|PROXY|SOCKS(?:4|5)?)\s+\S+))$/g
.test(str.trim())
);
if (!(ifValidArr.length && ifValid)) {
return showErrors(new TypeError(
'Неверный формат своих прокси. Свертесь с <a href="https://rebrand.ly/ac-own-proxy" data-in-bg="true">документацией</a>.'
));
}
oldMods[customProxyStringKey] = taVal;
} else {
localStorage.setItem(uiRaw, taVal);
}
}
conduct(
'Применяем настройки...',
(cb) => pacKitchen.keepCookedNowAsync(oldMods, cb),
'Настройки применены.',
() => {
document.getElementById('apply-mods').disabled = true;
}
);
};
document.getElementById('reset-mods').onclick = () => {
const ifSure = backgroundPage.confirm('Сбросить все модификаторы и ИСКЛЮЧЕНИЯ?');
if (!ifSure) {
return false;
}
conduct(
'Сбрасываем...',
(cb) => {
pacKitchen.resetToDefaults();
backgroundPage.utils.fireRequest('ip-to-host-reset-to-defaults', cb);
},
'Откройте окно заново для отображения эффекта.',
() => window.close()
);
};
}
// NOTIFICATIONS PANEL
const conPanel = document.getElementById('list-of-notifiers');
errorHandlers.getEventsMap().forEach( (value, name) => {
const li = document.createElement('li');
li.innerHTML = `
<input type="checkbox" id="if-on-${name}"/>
<label for="if-on-${name}">${value}</label>`;
const box = li.querySelector('input');
box.checked = backgroundPage.apis.errorHandlers.isOn(name);
box.onclick = function() {
const id = this.id.replace('if-on-', '');
return backgroundPage.apis.errorHandlers.switch(
this.checked ? 'on' : 'off',
id
);
};
conPanel.appendChild(li);
});
if( !errorHandlers.ifControllable ) {
document.getElementById('which-extension').innerHTML
= backgroundPage.utils.messages.whichExtensionHtml();
document.querySelectorAll('.if-not-controlled').forEach( (node) => {
node.style.display = 'block';
});
}
setStatusTo('');
if (antiCensorRu.ifFirstInstall) {
const id = antiCensorRu.getCurrentPacProviderKey() || 'none';
document.querySelector('#update-' + id).click();
}
document.documentElement.style.display = 'initial';
console.log(Date.now() - START);
})
);

View File

@ -16,7 +16,8 @@ chrome.runtime.getBackgroundPage( (backgroundPage) =>
# Комментарии НЕ сохраняются! # Комментарии НЕ сохраняются!
# Сначала идёт список проксируемых сайтов, # Сначала идёт список проксируемых сайтов,
# затем ==== на отдельной строке, # затем ==== на отдельной строке,
# затем исключённые сайты, отсортированные с конца строки. # затем исключённые сайты.
# Сортировка с конца строки.
# ПРОКСИРОВАТЬ: # ПРОКСИРОВАТЬ:

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

View File

@ -0,0 +1,346 @@
/*
* Copyright 2014 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* This stylesheet is used to apply Chrome styles to extension pages that opt in
* to using them.
*
* These styles have been copied from ui/webui/resources/css/chrome_shared.css
* and ui/webui/resources/css/widgets.css *with CSS class logic removed*, so
* that it's as close to a user-agent stylesheet as possible.
*
* For example, extensions shouldn't be able to set a .link-button class and
* have it do anything.
*
* Other than that, keep this file and chrome_shared.css/widgets.cc in sync as
* much as possible.
*/
body {
color: #333;
cursor: default;
/* Note that the correct font-family and font-size are set in
* extension_fonts.css. */
/* This top margin of 14px matches the top padding on the h1 element on
* overlays (see the ".overlay .page h1" selector in overlay.css), which
* every dialogue has.
*
* Similarly, the bottom 14px margin matches the bottom padding of the area
* which hosts the buttons (see the ".overlay .page * .action-area" selector
* in overlay.css).
*
* Both have a padding left/right of 17px.
*
* Note that we're putting this here in the Extension content, rather than
* the WebUI element which contains the content, so that scrollbars in the
* Extension content don't get a 6px margin, which looks quite odd.
*/
margin: 14px 17px;
}
p {
line-height: 1.8em;
}
h1,
h2,
h3 {
-webkit-user-select: none;
font-weight: normal;
/* Makes the vertical size of the text the same for all fonts. */
line-height: 1;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.3em;
margin-bottom: 0.4em;
}
h3 {
color: black;
font-size: 1.2em;
margin-bottom: 0.8em;
}
a {
color: rgb(17, 85, 204);
text-decoration: underline;
}
a:active {
color: rgb(5, 37, 119);
}
/* Default state **************************************************************/
:-webkit-any(button,
input[type='button'],
input[type='submit']),
select,
input[type='checkbox'],
input[type='radio'] {
-webkit-appearance: none;
-webkit-user-select: none;
background-image: linear-gradient(#ededed, #ededed 38%, #dedede);
border: 1px solid rgba(0, 0, 0, 0.25);
border-radius: 2px;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08),
inset 0 1px 2px rgba(255, 255, 255, 0.75);
color: #444;
font: inherit;
margin: 0 1px 0 0;
outline: none;
text-shadow: 0 1px 0 rgb(240, 240, 240);
}
:-webkit-any(button,
input[type='button'],
input[type='submit']),
select {
min-height: 2em;
min-width: 4em;
/*<if expr="is_win">
/* The following platform-specific rule is necessary to get adjacent
* buttons, text inputs, and so forth to align on their borders while also
* aligning on the text's baselines. */
padding-bottom: 1px;
/*</if>*/
}
:-webkit-any(button,
input[type='button'],
input[type='submit']) {
-webkit-padding-end: 10px;
-webkit-padding-start: 10px;
}
select {
-webkit-appearance: none;
-webkit-padding-end: 20px;
-webkit-padding-start: 6px;
/* OVERRIDE */
background-image: /*url(../../../ui/webui/resources/images/select.png),*/
linear-gradient(#ededed, #ededed 38%, #dedede);
background-position: right center;
background-repeat: no-repeat;
}
html[dir='rtl'] select {
background-position: center left;
}
input[type='checkbox'] {
height: 13px;
position: relative;
vertical-align: middle;
width: 13px;
}
input[type='radio'] {
/* OVERRIDE */
border-radius: 100%;
height: 15px;
position: relative;
vertical-align: middle;
width: 15px;
}
/* TODO(estade): add more types here? */
input[type='number'],
input[type='password'],
input[type='search'],
input[type='text'],
input[type='url'],
input:not([type]),
textarea {
border: 1px solid #bfbfbf;
border-radius: 2px;
box-sizing: border-box;
color: #444;
font: inherit;
margin: 0;
/* Use min-height to accommodate addditional padding for touch as needed. */
min-height: 2em;
padding: 3px;
outline: none;
<if expr="is_win or is_macosx or is_ios">
/* For better alignment between adjacent buttons and inputs. */
padding-bottom: 4px;
</if>
}
input[type='search'] {
-webkit-appearance: textfield;
/* NOTE: Keep a relatively high min-width for this so we don't obscure the end
* of the default text in relatively spacious languages (i.e. German). */
min-width: 160px;
}
/* Checked ********************************************************************/
input[type='checkbox']:checked::before {
-webkit-user-select: none;
background-image: url(./check.png);
background-size: 100% 100%;
content: '';
display: block;
height: 100%;
width: 100%;
}
input[type='radio']:checked::before {
background-color: #666;
border-radius: 100%;
bottom: 3px;
content: '';
display: block;
left: 3px;
position: absolute;
right: 3px;
top: 3px;
}
/* Hover **********************************************************************/
:enabled:hover:-webkit-any(
select,
input[type='checkbox'],
input[type='radio'],
:-webkit-any(
button,
input[type='button'],
input[type='submit'])) {
background-image: linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
border-color: rgba(0, 0, 0, 0.3);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12),
inset 0 1px 2px rgba(255, 255, 255, 0.95);
color: black;
}
:enabled:hover:-webkit-any(select) {
/* OVERRIDE */
background-image: /*url(../../../ui/webui/resources/images/select.png)*/,
linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
}
/* Active *********************************************************************/
:enabled:active:-webkit-any(
select,
input[type='checkbox'],
input[type='radio'],
:-webkit-any(
button,
input[type='button'],
input[type='submit'])) {
background-image: linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
box-shadow: none;
text-shadow: none;
}
:enabled:active:-webkit-any(select) {
/* OVERRIDE */
background-image: /*url(../../../ui/webui/resources/images/select.png),*/
linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
}
/* Disabled *******************************************************************/
:disabled:-webkit-any(
button,
input[type='button'],
input[type='submit']),
select:disabled {
background-image: linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6);
border-color: rgba(80, 80, 80, 0.2);
box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08),
inset 0 1px 2px rgba(255, 255, 255, 0.75);
color: #aaa;
}
select:disabled {
/* OVERRIDE */
background-image: /*url(../../../ui/webui/resources/images/disabled_select.png),*/
linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6);
}
input:disabled:-webkit-any([type='checkbox'],
[type='radio']) {
opacity: .75;
}
input:disabled:-webkit-any([type='password'],
[type='search'],
[type='text'],
[type='url'],
:not([type])) {
color: #999;
}
/* Focus **********************************************************************/
:enabled:focus:-webkit-any(
select,
input[type='checkbox'],
input[type='number'],
input[type='password'],
input[type='radio'],
input[type='search'],
input[type='text'],
input[type='url'],
input:not([type]),
:-webkit-any(
button,
input[type='button'],
input[type='submit'])) {
/* OVERRIDE */
-webkit-transition: border-color 200ms;
/* We use border color because it follows the border radius (unlike outline).
* This is particularly noticeable on mac. */
border-color: rgb(77, 144, 254);
outline: none;
}
/* Checkbox/radio helpers ******************************************************
*
* .checkbox and .radio classes wrap labels. Checkboxes and radios should use
* these classes with the markup structure:
*
* <div class="checkbox">
* <label>
* <input type="checkbox"></input>
* <span>
* </label>
* </div>
*/
:-webkit-any(.checkbox, .radio) label {
/* Don't expand horizontally: <http://crbug.com/112091>. */
align-items: center;
display: inline-flex;
padding-bottom: 7px;
padding-top: 7px;
}
:-webkit-any(.checkbox, .radio) label input {
flex-shrink: 0;
}
:-webkit-any(.checkbox, .radio) label input ~ span {
-webkit-margin-start: 0.6em;
/* Make sure long spans wrap at the same horizontal position they start. */
display: block;
}
:-webkit-any(.checkbox, .radio) label:hover {
color: black;
}
label > input:disabled:-webkit-any([type='checkbox'], [type='radio']) ~ span {
color: #999;
}

View File

@ -0,0 +1 @@
ChromeStyle: https://cs.chromium.org/chromium/src/extensions/renderer/resources/extension.css

View File

@ -0,0 +1,8 @@
[ignore]
.*/test/.*
[include]
[libs]
[options]

View File

@ -0,0 +1,4 @@
node_modules
npm-debug.log
*.swp
dist

View File

@ -0,0 +1,11 @@
# React + Flow + Webpack 2 Boilerplate with Babel
Switch branches for adding/removing Babel for JSX + Flowtype.
## Install
`yarn install` or `npm install --dev`
## Run
`yarn/npm run build`

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html style="display: none; will-change: contents, display">
<head>
<meta charset="utf-8">
<title>Настройки</title>
<link rel="stylesheet" href="../lib/chrome-style/index.css">
<style><!-- Don't delete this mount point! --></style>
</head>
<body>
<div id="app-root"></div>
<svg style="display: none" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<symbol id="iconInfo" viewBox="0 0 100 100">
<title>info</title>
<circle shape-rendering="geometricPrecision" fill="none" stroke="currentColor" stroke-width="7" cx="50" cy="50" r="45"/>
<path shape-rendering="crispEdges" d="M 55,40 V 80 H 45 V 40 z m 0,-20 V 35 H 45 V 20 Z"/>
</symbol>
<symbol id="iconLoopRound" viewBox="0 0 32 32">
<title>loop-round</title>
<path d="M27.802 5.197c-2.925-3.194-7.13-5.197-11.803-5.197-8.837 0-16 7.163-16 16h3c0-7.18 5.82-13 13-13 3.844 0 7.298 1.669 9.678 4.322l-4.678 4.678h11v-11l-4.198 4.197z"/>
<path d="M29 16c0 7.18-5.82 13-13 13-3.844 0-7.298-1.669-9.678-4.322l4.678-4.678h-11v11l4.197-4.197c2.925 3.194 7.13 5.197 11.803 5.197 8.837 0 16-7.163 16-16h-3z"/>
</symbol>
<symbol id="iconImportExport" viewBox="0 0 32 32">
<title>import-export</title>
<g transform="rotate(0 16 16)">
<path d="M7 22 h 25 v 4 h -25 v 5 l -7-7 7-7 v5 z"/>
<path d="M25 10 h-25 v-4 h 25 v -5 l 7 7 -7 7 z"/>
</g>
<!-- With bars on peaks.
<path d="M30 0h2v16h-2v-16z"></path>
<path d="M0 16h2v16h-2v-16z"></path>
<path d="M10 22 h 22 v 4 h -22 v 5 l -7-7 7-7 v5 z"></path>
<path d="M22 10 h-22 v-4 h 22 v -5 l 7 7 -7 7 z"></path-->
</symbol>
</svg>
<script src="./dist/bundle.min.js"></script>
<script src="../lib/keep-links-clickable.js"></script>
</body>
</html>

View File

@ -0,0 +1,24 @@
'use strict';
const loaderUtils = require('loader-utils');
const concat = require('concat-stream');
module.exports = function(content) {
const cb = this.async();
const Readable = require('stream').Readable;
const src = new Readable();
src._read = function noop() {};
src.push(content);
src.push(null);
const opts = loaderUtils.getOptions(this) || {};
const readme = Object.keys(opts).reduce((readable, moduleName) => {
const newStream = require(moduleName)(/* No filename. */);
return readable.pipe(newStream);
}, src);
readme.pipe(concat ((buf) => cb(null, buf.toString()) ));
};

View File

@ -0,0 +1,32 @@
{
"name": "hello-react",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-loader": "^7.0.0",
"babel-plugin-dynamic-import-webpack": "^1.0.1",
"babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.24.1",
"concat-stream": "^1.6.0",
"csjs-inject": "^1.0.1",
"flow-bin": "^0.45.0",
"inferno": "^3.2.0",
"inferno-component": "^3.1.2",
"inferno-create-element": "^3.1.2",
"webpack": "^2.5.1"
},
"scripts": {
"check": "flow status",
"build:prod": "webpack --define process.env.NODE_ENV=\"'production'\" --env=prod",
"build:dev:nocomp": "NODE_ENV=development webpack --define process.env.NODE_ENV=\"'development'\" --env=dev",
"build:dev": "NODE_ENV=development webpack --debug --define process.env.NODE_ENV=\"'development'\" --output-pathinfo --env=dev",
"gulp": "cd .. && npm run gulp",
"build": "npm run build:prod",
"start": "cd .. && npm start"
},
"dependencies": {
"babel-plugin-inferno": "^3.2.0"
}
}

View File

@ -0,0 +1,299 @@
import Inferno from 'inferno';
import Component from 'inferno-component';
import createElement from 'inferno-create-element';
import getNotControlledWarning from './NotControlledWarning';
import getMain from './Main';
import getFooter from './Footer';
export default function getApp(theState) {
const NotControlledWarning = getNotControlledWarning(theState);
const Main = getMain(theState);
const Footer = getFooter(theState);
return class App extends Component {
constructor(props) {
super(props);
const hash = window.location.hash.substr(1);
const hashParams = new URLSearchParams(hash);
this.state = {
status: 'Загрузка...',
ifInputsDisabled: false,
hashParams: hashParams,
};
this.setStatusTo = this.setStatusTo.bind(this);
this.conduct = this.conduct.bind(this);
this.showErrors = this.showErrors.bind(this);
this.showNews = this.showNews.bind(this);
}
setStatusTo(msg, cb) {
this.setState(
{
status: msg,
},
cb
);
}
setNewsStatusTo(newsArr) {
this.setStatusTo(
<ol style="list-style-type: initial;">
{newsArr.map(([title, url]) => (<li><a href={url}>{title}</a></li>))}
</ol>
);
}
async showNews() {
const uiComDate = 'ui-last-comment-date';
const uiComEtag = 'ui-last-comments-etag';
const uiLastNewsArr = 'ui-last-news-arr';
const statusFromHash = this.state.hashParams.get('status');
if (statusFromHash) {
return this.setStatusTo(statusFromHash);
}
const comDate = localStorage[uiComDate];
const query = comDate ? `?since=${comDate}` : '';
const oldEtag = localStorage[uiComEtag];
const headers = {
'User-Agent': 'anticensorship-russia',
};
if (oldEtag) {
Object.assign(headers, {
'If-None-Match': oldEtag,
});
};
const params = {
headers: new Headers(headers),
};
const ghUrl = `https://api.github.com/repos/anticensority/chromium-extension/issues/10/comments${query}`;
const [error, comments, etag] = await fetch(
ghUrl,
params
).then(
(res) => !( res.status >= 200 && res.status < 300 || res.status === 304 )
? Promise.reject({message: `Получен ответ с неудачным кодом ${res.status}.`, data: res})
: res
).then(
(res) => Promise.all([
null,
res.status !== 304 ? res.json() : false,
res.headers.get('ETag')
]),
(err) => {
const statusCode = err.data && err.data.status;
const ifCritical = null;
this.showErrors(ifCritical, {
message: statusCode === 403
? 'Слишком много запросов :-( Сообщите разработчику, как вам удалось всё истратить.'
: 'Не удалось достать новости: что-то не так с сетью.',
wrapped: err,
});
return [err, false, false];
}
);
if (etag) {
localStorage[uiComEtag] = etag;
}
if (error) {
return; // Let the user see the error message and contemplate.
}
const ifNewsWasSet = (() => {
if (comments === false) {
// 304
const json = localStorage[uiLastNewsArr];
const news = json && JSON.parse(json);
if (news && news.length) {
this.setNewsStatusTo(news);
return true;
}
return false;
}
let minDate; // Minimal date among all news-comments.
const news = comments.reduce((acc, comment) => {
const curDate = comment.updated_at || comment.created_at;
const newsTitle = this.getNewsHeadline( comment.body );
if (newsTitle) {
if (!minDate || curDate <= minDate) {
minDate = curDate;
}
acc.push([newsTitle, comment.html_url]);
}
return acc;
}, []);
// Response with empty news is cached too.
localStorage[uiLastNewsArr] = JSON.stringify(news);
if (news.length) {
if (minDate) {
localStorage[uiComDate] = minDate;
}
this.setNewsStatusTo(news);
return true;
}
return false;
})();
if (!ifNewsWasSet) {
this.setStatusTo('Хорошего настроения Вам!');
}
}
componentDidMount() {
if (!this.props.apis.antiCensorRu.ifFirstInstall) {
this.showNews();
}
}
getNewsHeadline(comBody) {
const headline = comBody.split(/\r?\n/)[0];
const ifOver = /#+\s*$/.test(headline);
if (ifOver) {
return false;
}
return headline.replace(/^\s*#+\s*/g, '');
}
showErrors(err, ...args/* ...warns, cb */) {
const lastArg = args[args.length - 1];
const cb = (lastArg && typeof lastArg === 'function')
? args.pop()
: () => {};
const warns = args;
const errToHtmlMessage = (error) => {
let messageHtml = '';
let wrapped = error.wrapped;
messageHtml = error.message || '';
while( wrapped ) {
const deeperMsg = wrapped && wrapped.message;
if (deeperMsg) {
messageHtml = messageHtml + ' &gt; ' + deeperMsg;
}
wrapped = wrapped.wrapped;
}
return messageHtml;
};
let messageHtml = err ? errToHtmlMessage(err) : '';
const warningHtml = warns
.filter((w) => w)
.map(
(w) => errToHtmlMessage(w)
)
.map( (m) => '✘ ' + m )
.join('<br/>');
messageHtml = messageHtml.trim();
if (warningHtml) {
messageHtml = messageHtml ? messageHtml + '<br/>' + warningHtml : warningHtml;
}
this.setStatusTo(
(<span>
<span style="color:red">
{err ? <span><span class="emoji">🔥</span> Ошибка!</span> : 'Некритичная oшибка.'}
</span>
<br/>
<span style="font-size: 0.9em; color: darkred" dangerouslySetInnerHTML={{__html: messageHtml}}></span>
{' '}
{err && <a href="" onClick={(evt) => {
this.props.apis.errorHandlers.viewError('pup-ext-err', err);
evt.preventDefault();
}}>[Техн.детали]</a>}
</span>),
cb
);
}
switchInputs(val) {
this.setState({
ifInputsDisabled: val === 'off' ? true : false,
});
}
conduct(
beforeStatus, operation, afterStatus,
onSuccess = () => {}, onError = () => {}
) {
this.setStatusTo(beforeStatus);
this.switchInputs('off');
operation((err, res, ...warns) => {
warns = warns.filter( (w) => w );
if (err || warns.length) {
this.showErrors(err, ...warns);
} else {
this.setStatusTo(afterStatus);
}
this.switchInputs('on');
if (!err) {
onSuccess(res);
} else {
onError(err);
}
});
}
render(originalProps) {
const props = Object.assign({}, originalProps, {
funs: {
setStatusTo: this.setStatusTo,
conduct: this.conduct,
showErrors: this.showErrors,
showNews: this.showNews,
},
ifInputsDisabled: this.state.ifInputsDisabled,
hashParams: this.state.hashParams,
});
return createElement('div', null, [
...( props.flags.ifNotControlled ? [createElement(NotControlledWarning, props)] : [] ),
createElement(Main, props),
createElement(Footer, Object.assign({ status: this.state.status }, props)),
]);
}
}
};

View File

@ -0,0 +1,37 @@
import Inferno, { linkEvent } from 'inferno';
export default function getApplyMods(theState) {
const resetMods = function resetMods(props) {
const ifSure = props.bgWindow.confirm('Сбросиь все модификаторы и ИСКЛЮЧЕНИЯ?');
if (!ifSure) {
return false;
}
props.funs.conduct(
'Сбрасываем...',
(cb) => {
props.apis.pacKitchen.resetToDefaults();
props.bgWindow.utils.fireRequest('ip-to-host-reset-to-defaults', cb);
window.localStorage.clear();
},
'Откройте окно заново для отображения эффекта.',
() => window.close()
);
}
return function ApplyMods(props) {
return (
<section class="controlRow horFlex" style="margin-top: 1em">
<input type="button" value="Применить" disabled={props.ifInputsDisabled} onClick={props.onClick}/>
<a href="" onClick={linkEvent(props, resetMods)}>К изначальным!</a>
</section>
);
};
};

View File

@ -0,0 +1,351 @@
import Inferno from 'inferno';
import Component from 'inferno-component';
import css from 'csjs-inject';
export default function getExcEditor(theState) {
const scopedCss = css`
#exc-address-container {
display: flex;
align-items: center;
width: 100%;
}
#exc-address-container > a {
border-bottom: 1px solid transparent;
margin-left: 0.2em;
align-self: flex-end;
}
#exc-address {
width: 100%;
display: flex;
align-items: baseline;
--exc-hieght: 1.6em;
font-size: 1em;
border-bottom: 1px solid var(--ribbon-color) !important;
}
input#exc-editor {
border: none;
width: 100%;
background: inherit;
/* The two below align '.' (dot) vertically. */
max-height: var(--exc-hieght) !important;
min-height: var(--exc-hieght) !important;
}
#exc-radio {
display: flex;
justify-content: space-around;
margin-top: 0.5em;
}
[name="if-proxy-this-site"]:checked + label {
font-weight: bold;
}
#exc-address.ifYes {
background-color: lightgreen;
}
#exc-address.ifNo {
background-color: pink;
}
`;
const labelIfProxied = '✔';
const labelIfNotProxied = '✘';
const labelIfAuto = '↻';
/* Not used.
const sortOptions = (options) => {
const aWins = 1;
return options.sort(([aHost, aState], [bHost, bState]) => aState === undefined ? aWins : aHost.localeCompare(bHost))
};
*/
return class ExcEditor extends Component {
modsToOpts(pacMods) {
return Object.keys(pacMods.exceptions || {}).sort().map(
(excHost) => [excHost, pacMods.exceptions[excHost]]
);
}
constructor(props) {
super(props);
const pacMods = props.apis.pacKitchen.getPacMods();
this.state = {
trimmedInputValueOrSpace:
props.currentTab && !props.currentTab.url.startsWith('chrome') ? new URL(props.currentTab.url).hostname : '',
sortedListOfOptions: this.modsToOpts(pacMods),
isHostHidden: {}
};
this.handleRadioClick = this.handleRadioClick.bind(this);
this.handleInputOrClick = this.handleInputOrClick.bind(this);
}
hideAllOptions() {
this.setState({
isHostHidden: this.state.sortedListOfOptions.reduce(
(isHostHidden, [excHost]) => {
isHostHidden[excHost] = true;
return isHostHidden;
},
{}),
});
}
isHostValid(host) {
const ValidHostnameRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
if(!ValidHostnameRegex.test(host)) {
this.props.funs.showErrors(new TypeError('Должно быть только доменное имя, без протокола, порта и пути. Попробуйте ещё раз.'));
return false;
}
return true;
}
handleRadioClick(event) {
const host = this.state.trimmedInputValueOrSpace;
(() => { // `return` === `preventDefault`.
if(!this.isHostValid(host)) {
return false;
}
const pacMods = this.props.apis.pacKitchen.getPacMods();
pacMods.exceptions = pacMods.exceptions || {};
let ifYesClicked = false;
switch(event.target.id) {
case 'this-auto':
delete pacMods.exceptions[host];
break;
case 'this-yes':
ifYesClicked = true;
case 'this-no':
if (ifYesClicked && !pacMods.filteredCustomsString) {
this.props.funs.showErrors( new TypeError(
'Проксировать СВОИ сайты можно только при наличии СВОИХ прокси. Нет своих прокси, удовлетворяющих вашим требованиям.'
));
return false;
}
pacMods.exceptions[host] = ifYesClicked;
break;
default:
throw new Error('Only yes/no/auto!');
}
this.props.funs.conduct(
'Применяем исключения...',
(cb) => this.props.apis.pacKitchen.keepCookedNowAsync(pacMods, cb),
'Исключения применены. Не забывайте о кэше!',
() => this.setState({sortedListOfOptions: this.modsToOpts(pacMods)})
);
})();
// Don't check before operation is finished.
event.preventDefault();
}
handleKeyDown(event) {
if(event.key === 'Enter') {
this.hideAllOptions();
}
return true;
}
handleInputOrClick(event) {
// Episode 1.
const ifClick = event && event.type === 'click';
// If triangle button on right of datalist input clicked.
let ifTriangleClicked = false;
{
const maxIndentFromRightInPx = 15;
ifTriangleClicked = ifClick
&& !this.rawInput.selectionStart && !this.rawInput.selectionEnd
&& event.x > this.rawInput.getBoundingClientRect().right - maxIndentFromRightInPx;
}
const setInputValue = (newValue) => {
if (ifClick && !ifTriangleClicked) {
// Don't jerk cursor on simple clicks.
return;
}
// See bug in my comment to http://stackoverflow.com/a/32394157/521957
// First click on empty input may be still ignored.
const newPos = this.rawInput.selectionStart + newValue.length - this.rawInput.value.length;
this.rawInput.value = newValue;
window.setTimeout(() => this.rawInput.setSelectionRange(newPos, newPos), 0);
};
const trimmedInput = event.target.value.trim();
const ifInputEmpty = !trimmedInput;
const ifInit = !event;
const currentHost = ifTriangleClicked ? '' : (trimmedInput || (ifInit ? '' : ' '));
setInputValue(currentHost);
this.setState({trimmedInputValueOrSpace: currentHost});
// Episode 2.
let exactHost, exactState; // Undefined.
let editedHost = false;
const hidden = this.state.sortedListOfOptions.reduce(
(hiddenAcc, [excHost, excState]) => {
if (excState === undefined) {
editedHost = excHost;
} else if (excHost === trimmedInput) {
// Exact match found for input.
[exactHost, exactState] = [excHost, excState];
}
hiddenAcc[excHost] = false;
return hiddenAcc;
},
{}
);
let options = this.state.sortedListOfOptions;
const removeEditedHost = () => {
options = options.filter(([excHost, excState]) => editedHost !== excHost);
delete hidden[editedHost];
};
(() => {// `return` === setState
if (ifTriangleClicked || ifInputEmpty) {
// Show all opts.
if (editedHost) {
// Example of editedOpt.value: 'abcde ' <- Mind the space (see unhideOptAndAddSpace)!
const ifBackspacedOneChar = ifInputEmpty && editedHost.length < 3;
if (ifBackspacedOneChar) {
removeEditedHost();
}
}
return true;
}
if (editedHost) {
const ifUpdateNeeded = editedHost !== trimmedInput;
if(!ifUpdateNeeded) {
hidden[editedHost] = true;
return true;
}
// Not exact! Update!
removeEditedHost();
}
if (!exactHost) {
editedHost = trimmedInput;
options.unshift([editedHost, undefined]);
if (!ifClick) {
// New value was typed -- don't show tooltip.
hidden[editedHost] = true;
}
}
// Exact found!
// Do nothing.
})();
this.setState({
isHostHidden: hidden,
sortedListOfOptions: options,
});
}
render(props) {
const inputProxyingState = this.state.sortedListOfOptions.reduce((acc, [excHost, excState]) => {
if ( acc !== undefined ) {
return acc;
}
return this.state.trimmedInputValueOrSpace === excHost ? excState : undefined;
}, undefined);
return (
<section style="padding-bottom: 1em;">
<div>Проксировать указанный сайт?</div>
<div id="exc-address-container">
<div id="exc-address" class={inputProxyingState !== undefined ? ( inputProxyingState === true ? scopedCss.ifYes : scopedCss.ifNo ) : ''}>
<span>*.</span><input placeholder="navalny.com" list="exc-list" id="exc-editor"
value={this.state.trimmedInputValueOrSpace}
ref={(inputNode) => { this.rawInput = inputNode; }}
onKeyDown={this.handleKeyDown.bind(this)}
onInput={this.handleInputOrClick}
onClick={this.handleInputOrClick}
/>
</div>
{/*<a href class="emoji">⇄</a>*/}
<a href="../exceptions/index.html" title="импорт/экспорт"><svg
class="icon"
><use xlink:href="#iconImportExport"></use></svg>
</a>
</div>
<datalist id="exc-list">
{
this.state.sortedListOfOptions.map(([excHost, excState]) => {
// 1. Option's value may be changed to hide it from the tooltip.
// 2. Space is used in matching so even an empty input (replaced with space) has tooltip with prompts.
return <option
value={ this.state.isHostHidden[excHost] ? '\n' : excHost + ' ' }
label={ excState === true ? labelIfProxied : (excState === false ? labelIfNotProxied : labelIfAuto) }/>
})
}
</datalist>
<ol class="horizontalList middledChildren" id="exc-radio">
<li><input id="this-auto" type="radio" checked name="if-proxy-this-site" onClick={this.handleRadioClick}/>{' '}
<label for="this-auto">{/*<span class="emoji">🔄(looks fat)</span>*/}<svg
class="icon"
style="position: relative; top: 0.15em;"><use xlink:href="#iconLoopRound"></use></svg>&nbsp;авто</label>
</li>
<li>
<input id="this-yes" type="radio" name="if-proxy-this-site" checked={inputProxyingState === true} onClick={this.handleRadioClick}/>
{' '}<label for="this-yes">
<span
class="emoji____buggy"
></span>&nbsp;да
</label>
</li>
<li>
<input id="this-no" type="radio" name="if-proxy-this-site" checked={inputProxyingState === false} onClick={this.handleRadioClick}/>
{' '}<label for="this-no"><span class="emoji"></span>&nbsp;нет</label></li>
</ol>
</section>
);
}
};
};

View File

@ -0,0 +1,90 @@
import Inferno from 'inferno';
import createElement from 'inferno-create-element';
import css from 'csjs-inject';
import getInfoLi from './InfoLi';
import getExcEditor from './ExcEditor';
export default function getExceptions(theState) {
const scopedCss = css`
.excMods {
padding-top: 1em;
}
.excMods input#mods-ifMindExceptions:not(:checked) + * > label {
color: red;
}
`;
const InfoLi = getInfoLi(theState);
const ExcEditor = getExcEditor(theState);
return function Exceptions(props) {
const applyMods = (newMods) => {
props.apis.pacKitchen.keepCookedNowAsync(newMods, (err, ...warns) =>
err
? props.funs.showErrors(err, ...warns)
: props.funs.setStatusTo('Применено.')
);
};
return props.flags.ifInsideOptionsPage
? (
<div class="nowrap">
Редактор исключений доступен только для <a href="chrome://newtab">вкладок</a>.
</div>)
:
(<div>
{createElement(ExcEditor, props)}
<ul class={scopedCss.excMods}>
{
props.apis.pacKitchen.getOrderedConfigs('exceptions').map((conf) => {
return <InfoLi
type="checkbox"
conf={conf}
idPrefix="mods-"
checked={conf.value}
disabled={props.ifInputsDisabled}
onClick={(evt) => {
const oldMods = props.apis.pacKitchen.getPacMods();
oldMods[conf.key] = !conf.value;
applyMods(oldMods);
}}
/>;
})
}
{
!props.flags.ifMini && (
<InfoLi
type="checkbox"
conf={{
label: '<span>Собирать <a href="../errors-to-exc/index.html">последние ошибки</a> сайтов</span>',
key: 'lookupLastErrors',
desc: 'Собирать последние ошибки в запросах, чтобы вручную добавлять избранные из них в исключения.',
}}
checked={props.bgWindow.apis.lastNetErrors.ifCollecting}
onChange={(event) => {
props.bgWindow.apis.lastNetErrors.ifCollecting = event.target.checked;
props.funs.setStatusTo('Сделано.');
}}
/>
)
}
</ul>
</div>
);
};
};

View File

@ -0,0 +1,39 @@
import Inferno from 'inferno';
import css from 'csjs-inject';
export default function getFooter() {
const scopedCss = css`
.statusRow {
padding: 0 0.3em 1em;
}
.status {
display: inline-block;
}
.controlRow {
margin: 1em 0 1em 0;
}
`;
return function (props) {
return (
<div class="horPadded">
<section class={scopedCss.statusRow}>
<div clss={scopedCss.status} style="will-change: contents">{props.status}</div>
</section>
<footer class={scopedCss.controlRow + ' horFlex nowrap'}>
<input type="button" value="Готово" disabled={props.ifInputsDisabled} onClick={() => window.close()} />
<a href="../troubleshoot/index.html">
Проблемы?
</a>
</footer>
</div>
);
};
};

View File

@ -0,0 +1,156 @@
import Inferno from 'inferno';
import createElement from 'inferno-create-element';
import css from 'csjs-inject';
export default function getInfoLi() {
const scopedCss = css`
/* CONTROL RAW = BUTTON + LINK */
.labelContainer {
flex-grow: 9;
padding-left: 0.3em;
/* Vertical align to middle. */
/*align-self: flex-end;*/
line-height: 100%;
}
/* INFO SIGNS */
input:disabled + .labelContainer label {
color: var(--default-grey);
pointer-events: none;
}
.infoRow {
position: relative;
}
.infoRow > input[type="checkbox"] {
position: relative;
top: -0.08em;
}
.rightBottomIcon {
margin-left: 0.1em;
vertical-align: bottom;
}
.infoUrl, .infoUrl:hover {
text-decoration: none;
}
/* Source: https://jsfiddle.net/greypants/zgCb7/ */
.desc {
text-align: right;
color: var(--ribbon-color);
cursor: help;
padding-left: 0.3em;
}
.tooltip {
display: none;
position: absolute;
white-space: initial;
word-break: initial;
top: 100%;
left: 0;
right: 1em;
z-index: 1;
background-color: var(--ribbon-color);
padding: 1em;
color: white;
text-align: initial;
}
.desc:hover .tooltip {
display: block;
}
.tooltip a,
.tooltip em {
color: white;
}
.desc .tooltip:after {
border-left: solid transparent 0.5em;
border-bottom: solid var(--ribbon-color) 0.5em;
position: absolute;
top: -0.5em;
content: "";
width: 0;
right: 0;
height: 0;
}
/* This bridges the gap so you can mouse into the tooltip without it disappearing. */
.desc .tooltip:before {
position: absolute;
top: -1em;
content: "";
display: block;
height: 1.2em;
left: 75%;
width: calc(25% + 0.6em);
}
/* CHILDREN */
.children {
flex-grow: 9999;
}
`;
const camelToDash = (name) => name.replace(/([A-Z])/g, (_, p1) => '-' + p1.toLowerCase());
// const dashToCamel = (name) => name.replace(/-(.)/g, (_, p1) => p1.toUpperCase());
const InfoIcon = function InfoIcon(props) {
return (
<svg class="icon" style="position: relative; top: 0.08em">$
<use xlink:href="#iconInfo"></use>$
</svg>
);
};
return function InfoLi(originalProps) {
const props = Object.assign({}, {
idPrefix: '',
ifDashify: false,
}, originalProps);
const iddy = props.idPrefix + ( props.ifDashify ? camelToDash(props.conf.key) : props.conf.key );
const inputProps = {
id: iddy,
name: props.name,
type: props.type,
checked: props.checked,
onClick: props.onClick,
onChange: props.onChange,
class: props.class,
disabled: props.ifInputsDisabled,
};
delete inputProps.children;
return (
<li class={scopedCss.infoRow + ' horFlex'} style={ props.children && 'flex-wrap: wrap'}>
{createElement('input', inputProps)}
<div class={scopedCss.labelContainer}>
<label for={iddy} dangerouslySetInnerHTML={{__html: props.conf.label}}></label>
{props.nodeAfterLabel}
</div>
{props.conf.desc
? (
<div class={scopedCss.desc}>
<InfoIcon />
<div class={scopedCss.tooltip} dangerouslySetInnerHTML={{__html: props.conf.desc}}/>
</div>)
: (props.conf.url
? (<a href={props.conf.url} class={[scopedCss.rightBottomIcon, scopedCss.infoUrl].join(' ')} title="Открыть документацию"><InfoIcon /></a>)
: (<span>&nbsp;</span>) // Affects vertical align of flexbox items.
)
}
{/* props.checked && props.children */}
{props.checked && props.children && (<div class={scopedCss.children}>{props.children}</div>)}
</li>
);
};
};

View File

@ -0,0 +1,62 @@
import Inferno from 'inferno';
import Component from 'inferno-component';
export default function getLastUpdateDate(theState) {
return class LastUpdateDate extends Component {
componentWillMount() {
this.onStorageChangedHandler = (changes) =>
changes.lastPacUpdateStamp.newValue && this.forceUpdate();
chrome.storage.onChanged.addListener( this.onStorageChangedHandler );
}
componentWillUnmount() {
chrome.storage.onChanged.removeListener( this.onStorageChangedHandler );
}
getDate(antiCensorRu) {
let dateForUser = 'никогда';
if( antiCensorRu.lastPacUpdateStamp ) {
let diff = Date.now() - antiCensorRu.lastPacUpdateStamp;
let units = 'мс';
const gauges = [
[1000, 'с'],
[60, 'мин'],
[60, 'ч'],
[24, 'дн'],
[7, ' недель'],
[4, ' месяцев'],
];
for(const g of gauges) {
const diffy = Math.floor(diff / g[0]);
if (!diffy)
break;
diff = diffy;
units = g[1];
}
dateForUser = diff + units + ' назад';
}
return {
text: `${dateForUser} / ${antiCensorRu.pacUpdatePeriodInMinutes/60}ч`,
title: new Date(antiCensorRu.lastPacUpdateStamp).toLocaleString('ru-RU'),
};
}
render(props) {
const date = this.getDate(props.apis.antiCensorRu);
return (<div>Обновлялись: <span class="updateDate" title={date.title}>{ date.text }</span></div>);
}
};
};

View File

@ -0,0 +1,176 @@
import Inferno, {linkEvent} from 'inferno';
import Component from 'inferno-component';
import createElement from 'inferno-create-element';
import css from 'csjs-inject';
import getTabPanel from './TabPanel';
import getPacChooser from './PacChooser';
import getExceptions from './Exceptions';
import getModList from './ModList';
import getProxyEditor from './ProxyEditor';
import getApplyMods from './ApplyMods';
import getNotifications from './Notifications';
export default function getMain(theState) {
const scopedCss = css`
input#ifProxyHttpsUrlsOnly:checked + div {
color: red;
}
`;
const TabPanel = getTabPanel(theState);
const PacChooser = getPacChooser(theState);
const Exceptions = getExceptions(theState);
const ModList = getModList(theState);
const ProxyEditor = getProxyEditor(theState);
const ApplyMods = getApplyMods(theState);
const Notifications = getNotifications(theState);
const checksName = 'pacMods';
return class Main extends Component {
constructor(props) {
super(props);
this.state = {
ifModsChangesStashed: false,
catToOrderedMods: {
'general': props.apis.pacKitchen.getOrderedConfigs('general'),
'ownProxies': props.apis.pacKitchen.getOrderedConfigs('ownProxies'),
},
};
this.handleModChange = this.handleModChange.bind(this);
}
getAllMods() {
return [].concat(...Object.keys(this.state.catToOrderedMods).map((cat) =>
this.state.catToOrderedMods[cat]
))
}
handleModApply(that) {
const modsMutated = that.props.apis.pacKitchen.getPacMods();
const newMods = that.getAllMods().reduce((_, conf) => {
modsMutated[conf.key] = conf.value;
return modsMutated;
}, modsMutated/*< Needed for index 0*/);
that.props.funs.conduct(
'Применяем настройки...',
(cb) => that.props.apis.pacKitchen.keepCookedNowAsync(newMods, cb),
'Настройки применены.',
() => that.setState({ifModsChangesStashed: false})
);
}
handleModChange({targetConf, targetIndex, newValue}) {
const oldCats = this.state.catToOrderedMods;
const newCats = Object.keys(this.state.catToOrderedMods).reduce((acc, cat) => {
if (cat !== targetConf.category) {
acc[cat] = oldCats[cat];
} else {
acc[cat] = oldCats[cat].map((conf, index) => {
if (targetIndex !== index) {
return conf;
}
return Object.assign({}, conf, {
value: newValue
});
});
}
return acc;
}, {});
this.setState({
catToOrderedMods: newCats,
ifModsChangesStashed: true,
});
}
render(props) {
const applyModsEl = createElement(ApplyMods, Object.assign({}, props,
{
ifInputsDisabled: !this.state.ifModsChangesStashed || props.ifInputsDisabled,
onClick: linkEvent(this, this.handleModApply),
}
));
const modsHandlers = {
onConfChanged: this.handleModChange,
};
return createElement(TabPanel, Object.assign({}, props, {
tabs: [
{
label: 'PAC-скрипт',
content: createElement(PacChooser, props),
key: 'pacScript',
},
{
label: 'Исключения',
content: createElement(Exceptions, props),
key: 'exceptions',
},
{
label: 'Свои прокси',
content: createElement(
ModList,
Object.assign({}, props, {
orderedConfigs: this.state.catToOrderedMods['ownProxies'],
childrenOfMod: {
customProxyStringRaw: ProxyEditor,
},
name: checksName,
}, modsHandlers)
),
key: 'ownProxies',
},
{
label: 'Модификаторы',
content: createElement(
ModList,
Object.assign({}, props, {
orderedConfigs: this.state.catToOrderedMods['general'],
name: checksName,
}, modsHandlers)
),
key: 'mods',
},
{
content: applyModsEl,
key: 'applyMods',
},
{
label: 'Уведомления',
content: createElement(Notifications, props),
key: 'notifications',
},
],
alwaysShownWith: {
'applyMods': ['ownProxies', 'mods'],
},
}));
}
}
};

View File

@ -0,0 +1,81 @@
import Inferno, {linkEvent} from 'inferno';
import Component from 'inferno-component';
import createElement from 'inferno-create-element';
import getInfoLi from './InfoLi';
export default function getModList(theState) {
const InfoLi = getInfoLi(theState);
return class ModList extends Component {
constructor(props) {
super(props);
this.state= {
checks: props.orderedConfigs.map((mod) => Boolean(mod.value)),
};
}
handleCheck(confMeta, ifChecked) {
this.setState({
checks: this.state.checks.map(
(ch, i) => i === confMeta.index ? ifChecked : ch
)
});
if (ifChecked === false || !confMeta.ifChild) {
this.handleNewValue(confMeta, ifChecked);
}
}
handleNewValue({ conf, index }, newValue) {
this.props.onConfChanged({
targetConf: conf,
targetIndex: index,
newValue: newValue,
});
}
render(props) {
return (
<ol>
{
props.orderedConfigs.map((conf, index) => {
const ifMayHaveChild = props.childrenOfMod && props.childrenOfMod[conf.key];
const confMeta = { conf, index, ifChild: ifMayHaveChild };
const child = ifMayHaveChild && this.state.checks[index]
&& createElement(
props.childrenOfMod[conf.key],
Object.assign({}, props, {conf, onNewValue: (newValue) => this.handleNewValue(confMeta, newValue)})
);
return (<InfoLi
conf={conf}
type='checkbox'
name={props.name}
checked={this.state.checks[index]}
key={index}
onChange={(event) => this.handleCheck(confMeta, event.target.checked)}
ifInputsDisabled={props.ifInputsDisabled}
>
{child}
</InfoLi>);
})
}
</ol>
);
}
}
};

View File

@ -0,0 +1,41 @@
// @flow
import css from 'csjs-inject';
import Inferno from 'inferno';
export default function getNotControlledWarning({ flags }) {
const cssClasses = css`
.warningContainer {
background-color: red;
color: white;
font-weight: bold;
text-align: center;
${ flags.ifInsideOptionsPage
? `
padding-top: 0;
padding-bottom: 0;
` : `
padding-top: 1em;
padding-bottom: 1em;
`
}
border-bottom: 1px solid var(--default-grey);
}
.warningContainer a {
color: white;
}
`;
return function NotControlledWarning(props) {
return (
<section class={cssClasses.warningContainer + " horPadded"} dangerouslySetInnerHTML={{ __html: props.utils.messages.whichExtensionHtml() }} />
);
}
}

View File

@ -0,0 +1,54 @@
import Inferno from 'inferno';
import css from 'csjs-inject';
export default function getPacChooser(theState) {
const scopedCss = css`
.listOfNotifiers {
margin-left: 0.4em;
}
`;
return function Notifications(props) {
return (
<section>
<header>Я <span class="emoji" style="color: #f93a17"></span> yведомления:</header>
<ul class={scopedCss.listOfNotifiers + ' middledChildren'}>
{
Array.from(props.apis.errorHandlers.getEventsMap()).map(([ntfId, ntfName]) => {
const iddy = `if-on-${ntfId}`;
const ifChecked = props.apis.errorHandlers.isOn(ntfId);
return (
<li>
<input
type="checkbox"
id={iddy}
checked={ifChecked}
disabled={props.ifInputsDisabled}
onClick={() => {
props.apis.errorHandlers.switch(
ifChecked ? 'off' : 'on', // Reverse.
ntfId
);
}}
/>
{' '}
<label for={iddy}>{ntfName}</label>
</li>
);
})
}
</ul>
</section>
);
};
};

View File

@ -0,0 +1,170 @@
import Inferno from 'inferno';
import Component from 'inferno-component';
import createElement from 'inferno-create-element';
import css from 'csjs-inject';
import getLastUpdateDate from './LastUpdateDate';
import getInfoLi from './InfoLi';
export default function getPacChooser(theState) {
const scopedCss = css`
/* OTHER VERSION */
.otherVersion {
font-size: 1.7em;
color: var(--ribbon-color);
margin-left: 0.1em;
}
.otherVersion:hover {
text-decoration: none;
}
.fullLineHeight,
.fullLineHeight * {
line-height: 100%;
}
/* TAB_1: PAC PROVIDER */
.updateButton {
visibility: hidden;
margin-left: 0.5em;
}
input:checked + div .updateButton {
visibility: inherit;
}
label[for="onlyOwnSites"] + .updateButton,
label[for="none"] + .updateButton {
display: none;
}
#none:checked + div label[for="none"] {
color: red;
}
#updateMessage {
white-space: nowrap;
margin-top: 0.5em;
}
`;
const LastUpdateDate = getLastUpdateDate(theState);
const InfoLi = getInfoLi(theState);
return class PacChooser extends Component {
constructor(props) {
super();
this.state = {
chosenPacName: 'none',
};
this.updatePac = function updatePac(onSuccess) {
props.funs.conduct(
'Обновляем...',
(cb) => props.apis.antiCensorRu.syncWithPacProviderAsync(cb),
'Обновлено.',
onSuccess
);
};
this.radioClickHandler = this.radioClickHandler.bind(this);
this.updateClickHandler = this.updateClickHandler.bind(this);
}
getCurrentProviderId() {
return this.props.apis.antiCensorRu.getCurrentPacProviderKey() || 'none';
}
updateClickHandler(event) {
event.preventDefault();
this.updatePac();
}
radioClickHandler(event) {
const checkChosenProvider = () =>
this.setState({ chosenPacName: this.getCurrentProviderId() });
const pacKey = event.target.id;
if (
pacKey === (
this.props.apis.antiCensorRu.getCurrentPacProviderKey() || 'none'
)
) {
return false;
}
if (pacKey === 'none') {
this.props.funs.conduct(
'Отключение...',
(cb) => this.props.apis.antiCensorRu.clearPacAsync(cb),
'Отключено.',
() => this.setState({ chosenPacName: 'none' }),
checkChosenProvider
);
} else {
this.props.funs.conduct(
'Установка...',
(cb) => this.props.apis.antiCensorRu.installPacAsync(pacKey, cb),
'PAC-скрипт установлен.',
checkChosenProvider
);
}
return false;
}
render(props) {
const iddyToCheck = this.getCurrentProviderId();
return (
<div>
{props.flags.ifInsideOptionsPage && (<header>PAC-скрипт:</header>)}
<ul>
{
[...props.apis.antiCensorRu.getSortedEntriesForProviders(), {key: 'none', label: 'Отключить'}].map((provConf) =>
(<InfoLi
onClick={this.radioClickHandler}
conf={provConf}
type="radio"
name="pacProvider"
checked={iddyToCheck === provConf.key}
ifInputsDisabled={props.ifInputsDisabled}
nodeAfterLabel={<a href="" class={scopedCss.updateButton} onClick={this.updateClickHandler}>[обновить]</a>}
/>)
)
}
</ul>
<div id="updateMessage" class="horFlex" style="align-items: center">
{ createElement(LastUpdateDate, props) }
<div class={scopedCss.fullLineHeight}>
{
props.flags.ifMini
? (<a class={scopedCss.otherVersion + ' emoji'} href="https://rebrand.ly/ac-versions"
title="Полная версия">🏋</a>)
: (<a class={scopedCss.otherVersion + ' emoji'} href="https://rebrand.ly/ac-versions"
title="Версия для слабых машин">🐌</a>)
}
</div>
</div>
</div>
);
}
componentDidMount() {
if (this.props.apis.antiCensorRu.ifFirstInstall) {
this.updatePac();
}
}
};
};

View File

@ -0,0 +1,580 @@
import Inferno, {linkEvent} from 'inferno';
import Component from 'inferno-component';
import createElement from 'inferno-create-element';
import css from 'csjs-inject';
export default function getProxyEditor(theState) {
const scopedCss = css`
table.editor {
border-collapse: collapse;
/*border-style: hidden;*/
width: 100%;
margin: 0.5em 0;
background-color: #f3f5f6;
}
.tabledEditor, .exportsEditor {
/* Empty, but not undefined. */
}
table.editor ::-webkit-input-placeholder {
color: #c9c9c9;
}
table.editor th.shrink,
table.editor td.shrink {
width: 1%;
}
table.editor td, table.editor th {
border: 1px solid #ccc;
text-align: left;
height: 100%;
}
table.editor input,
table.editor button,
table.editor select
{
min-width: 0;
min-height: 0;
height: 100%;
}
/* ADD PANEL */
table.editor tr.addPanel td,
table.editor tr.addPanel td input
{
padding: 0;
}
table.editor tr.addPanel td > select[name="newProxyType"],
table.editor tr.addPanel td:nth-last-child(2) input /* PORT */
{
font-size: 0.9em;
}
table.editor tr.addPanel td:nth-last-child(2) /* PORT */
{
min-width: 4em;
}
/* PROXY ROW */
table.editor tr.proxyRow td:nth-child(2), /* type */
table.editor tr.proxyRow td:nth-child(4) /* port */
{
text-align: center;
}
table.editor tr.proxyRow input[name="hostname"] {
padding: 0;
}
table.editor th:not(:last-child) {
padding: 0 0.6em;
}
table.editor input:not([type="submit"]),
table.editor select,
table.editor select:hover {
border: none;
background: inherit !important;
}
table.editor select,
table.editor select:hover {
-webkit-appearance: menulist !important;
box-shadow: none !important;
}
table.editor input {
width: 100%;
}
/* BUTTONS */
table.editor input[type="submit"],
table.editor button {
width: 100%;
padding: 0;
border: none;
}
.only {
/*height: 100%;*/
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
table.editor .add {
font-weight: 900;
}
table.editor .export {
/*padding-right: 2px;*/
}
/* LAST COLUMN: BUTTONS */
table.editor tr > *:nth-last-child(1), /* Buttons */
table.tabledEditor tr > *:nth-last-child(2), /* Port */
table.tabledEditor tr.proxyRow > td:first-child /* Type */
{
text-align: center;
padding: 0;
position: relative;
min-width: 1em;
}
/* LAST-2 COLUMN: HOSTNAME
table.editor td:nth-last-child(3) {
padding-left: 2px;
}*/
.noPad {
padding: 0;
}
.padLeft {
padding-left: 2px !important;
}
textarea.textarea {
width: 100% !important;
min-height: 100%;
height: 6em;
border-width: 1px 0 0 0;
/*border: none;*/
}
table.editor input:invalid {
color: red !important;
border-radius: 0;
border-bottom: 1px dotted red !important;
}
`;
const UI_RAW = 'ui-proxy-string-raw';
const MAX_PORT = 65535;
const onlyPort = function onlyPort(event) {
if (!event.ctrlKey && (/^\D$/.test(event.key) || /^\d$/.test(event.key) && parseInt(`${this.value}${event.key}`) > MAX_PORT)) {
event.preventDefault();
return false;
}
// Digits, Alt, Tab, Enter, etc.
return true;
};
const splitBySemi = (proxyString) => proxyString.replace(/#.*$/mg, '').trim().split(/\s*;\s*/g).filter((s) => s);
const joinBySemi = (strs) => strs.join(';\n') + ';';
const normilizeProxyString = (str) => joinBySemi(splitBySemi(str));
const PROXY_TYPE_LABEL_PAIRS = [['PROXY', 'PROXY/HTTP'],['HTTPS'],['SOCKS4'],['SOCKS5'],['SOCKS']];
const SwitchButton = (props) =>
(
<button
type="button" disabled={props.ifInputsDisabled}
class={'emoji' + ' ' + scopedCss.export + ' ' + scopedCss.only}
title={props.title}
onClick={props.onClick}
></button>
);
class TabledEditor extends Component {
constructor(props) {
super(props);
this.state = {
selectedNewType: 'HTTPS',
};
}
handleTypeSelect(that, event) {
that.setState({
selectedNewType: event.target.value,
});
}
showInvalidMessage(that, event) {
that.props.funs.showErrors({message: event.target.validationMessage});
}
handleModeSwitch(that) {
that.props.onSwitch();
}
handleAdd(that, event) {
const form = event.target;
const elements = Array.from(form.elements).reduce((acc, el, index) => {
acc[el.name || index] = el.value;
el.value = '';
return acc;
}, {});
const type = that.state.selectedNewType;
const hostname = elements.newHostname;
const port = elements.newPort;
const newValue = `${that.props.proxyStringRaw}; ${type} ${hostname}:${port}`
.trim().replace(/(\s*;\s*)+/, '; ');
that.props.setProxyStringRaw(newValue);
}
handleDelete(that, {proxyAsString, index}) {
event.preventDefault();
const proxyStrings = splitBySemi(that.props.proxyStringRaw);
proxyStrings.splice(index, 1);
that.props.setProxyStringRaw( joinBySemi(proxyStrings) );
}
raisePriority(that, {proxyAsString, index}) {
event.preventDefault();
if (index < 1) {
return;
}
const proxyStrings = splitBySemi(that.props.proxyStringRaw);
proxyStrings.splice(index - 1, 2, proxyStrings[index], proxyStrings[index-1]);
that.props.setProxyStringRaw( joinBySemi(proxyStrings) );
}
handleSubmit(that, event) {
event.preventDefault();
that.handleAdd(that, event);
}
render(props) {
return (
<form onSubmit={linkEvent(this, this.handleSubmit)}>
<table class={scopedCss.editor + ' ' + scopedCss.tabledEditor}>
<thead>
<tr>
<th colspan="2" class={scopedCss.shrink}>протокол</th>
<th>домен / IP</th>
<th class={scopedCss.shrink}>порт</th>
<th>
<SwitchButton title="импорт/экспорт" onClick={linkEvent(this, this.handleModeSwitch)}/>
</th>
</tr>
</thead>
<tbody>
{/* ADD NEW PROXY STARTS. */}
<tr class={scopedCss.addPanel}>
<td colspan="2">
<select reqiured
class={scopedCss.noPad}
name="newProxyType"
onChange={linkEvent(this, this.handleTypeSelect)}
>
{
PROXY_TYPE_LABEL_PAIRS.map(
([type, label]) =>
(<option value={type} selected={type === this.state.selectedNewType}>
{label || type}
</option>)
)
}
</select>
</td>
<td>
{/* LAST-2: HOSTNAME */}
<input required disabled={props.ifInputsDisabled}
class={scopedCss.noPad}
placeholder="89.140.125.17"
name="newHostname"
onInvalid={linkEvent(this, this.showInvalidMessage)}
tabindex="1"
/>
</td>
<td>
{/* LAST-1: PORT */}
<input required type="number" disabled={props.ifInputsDisabled}
class={scopedCss.noPad + ' ' + scopedCss.padLeft + ' ' + scopedCss.only}
placeholder="9150"
min="0" step="1" max={MAX_PORT} pattern="[0-9]{1,5}"
name="newPort"
onInvalid={linkEvent(this, this.showInvalidMessage)}
onkeydown={onlyPort}
tabindex="2"
/>
</td>
<td>
{/* LAST: ADD BUTTON */}
<input type="submit" disabled={props.ifInputsDisabled}
class={scopedCss.add + ' ' + scopedCss.only}
title="Добавить прокси" value="+"
/>
</td>
</tr>
{/* ADD NEW PROXY ENDS. */}
{
splitBySemi(this.props.proxyStringRaw).map((proxyAsString, index) => {
const [type, addr] = proxyAsString.trim().split(/\s+/);
const [hostname, port] = addr.split(':');
return (
<tr class={scopedCss.proxyRow}>
<td>
<button type="button" disabled={props.ifInputsDisabled}
class={scopedCss.only} title="Удалить"
onClick={() => this.handleDelete(this, {proxyAsString, index})}
>X</button>
</td>
<td>{type}</td>
<td><input value={hostname} name="hostname" readonly/></td>
<td>{port}</td>
<td>
<button type="button" disabled={props.ifInputsDisabled}
class={scopedCss.only} title="Повысить приоритет"
onClick={() => this.raisePriority(this, {proxyAsString, index})}
></button>
</td>
</tr>
);
})
}
</tbody>
</table>
</form>
);
}
}
const getInitState = () => ({
ifHasErrors: false,
stashedExports: false,
});
class ExportsEditor extends Component {
constructor(props) {
super(props);
this.state = getInitState();
}
resetState(that, event) {
that.setState(getInitState());
event.preventDefault();
}
getErrorsInStashedExports() {
if(this.state.stashedExports === false) {
return;
}
const errors = splitBySemi(this.state.stashedExports)
.map((proxyAsString) => {
const [rawType, addr, ...rest] = proxyAsString.split(/\s+/);
if (rest && rest.length) {
return new Error(
`"${rest.join(', ')}" кажется мне лишним. Вы забыли ";"?`
);
}
const knownTypes = PROXY_TYPE_LABEL_PAIRS.map(([type, label]) => type);
if( !knownTypes.includes(rawType.toUpperCase()) ) {
return new Error(
`Неверный тип ${rawType}. Известные типы: ${knownTypes.join(', ')}.`
);
}
if (!(addr && /^[^:]+:\d+$/.test(addr))) {
return new Error(
`Адрес прокси "${addr || ''}" не соответствует формату "<домен_или_IP>:<порт_из_цифр>".`
);
}
const [hostname, rawPort] = addr.split(':');
const port = parseInt(rawPort);
if (port < 0 || port > 65535) {
return new Error(
`Порт "${rawPort}" должен быть целым числом от 0 до 65535.`
);
}
return false;
}).filter((e) => e);
return errors && errors.length && errors;
}
handleModeSwitch(that, event) {
if (that.state.stashedExports !== false) {
const errors = that.getErrorsInStashedExports();
if (errors) {
that.setState({ifHasErrors: true});
that.props.funs.showErrors(...errors);
return;
}
that.props.setProxyStringRaw(that.state.stashedExports);
}
that.setState({
stashedExports: false,
ifHasErrors: false,
});
that.props.onSwitch();
}
handleTextareaChange(that, event) {
that.setState({
stashedExports: normilizeProxyString(event.target.value),
});
}
handleSubmit(that, event) {
event.preventDefault();
this.handleModeSwitch(this, event);
}
render(props) {
const reset = linkEvent(this, this.resetState);
return (
<form onSubmit={linkEvent(this, this.handleSubmit)}>
<table class={scopedCss.editor + ' ' + scopedCss.exportsEditor}>
<thead>
<tr>
<th style="width: 100%">
{
this.state.stashedExports === false
? 'Комментарии вырезаются!'
: (this.state.ifHasErrors
? (<span><a href="" onClick={reset}>Сбросьте изменения</a> или поправьте</span>)
: (<a href="" onClick={reset}>Сбросить изменения</a>)
)
}
</th>
<th style="width: 1%">
<SwitchButton title="Переключиться в табличный режим" onClick={linkEvent(this, this.handleModeSwitch)}/>
</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><textarea
class={scopedCss.textarea}
spellcheck={false}
placeholder={`
SOCKS5 localhost:9050; # Tor Expert
SOCKS5 localhost:9150; # Tor Browser
HTTPS 11.22.33.44:3143;
PROXY foobar.com:8080; # Not HTTP!`.trim()}
onChange={linkEvent(this, this.handleTextareaChange)}
value={
this.state.stashedExports !== false
? this.state.stashedExports
: (this.props.proxyStringRaw || '').replace(/\s*;\s*/g, ';\n')
}
/></td>
</tr>
</tbody>
</table>
</form>
);
}
}
const migrate = (proxyStringRaw) => {
/* In the old format \n\r? could be used as a separator. */
return proxyStringRaw
.replace(/#.*$/mg, '') // Strip comments.
.split( /(?:[^\S\r\n]*(?:;|\r?\n)+[^\S\r\n]*)+/g )
.map( (p) => p.trim() )
.filter((p) => p)
.join(';\n');
};
let waitingTillMount = [];
return class ProxyEditor extends Component {
constructor(props/*{ conf, onNewValue, ifInputsDisabled }*/) {
super(props);
const oldValue = typeof props.conf.value === 'string' && props.conf.value;
const newValue = migrate(oldValue || localStorage.getItem(UI_RAW) || '');
this.state = {
proxyStringRaw: newValue,
ifExportsMode: false,
};
this.handleSwitch = () => this.setState({ifExportsMode: !this.state.ifExportsMode});
waitingTillMount.push(newValue); // Wait till mount or eat bugs.
}
componentDidMount() {
if (waitingTillMount.length) {
this.mayEmitNewValue(this.props.value, waitingTillMount.pop());
waitingTillMount = [];
}
}
componentDidUnmount() {
waitingTillMount = [];
}
mayEmitNewValue(oldValue, newValue) {
if ( // Reject: 1) both `false` OR 2) both `===`.
( Boolean(oldValue) || Boolean(newValue) ) && oldValue !== newValue
) {
this.props.onNewValue(newValue);
}
}
render(originalProps) {
const props = Object.assign({
proxyStringRaw: this.state.proxyStringRaw,
onSwitch: this.handleSwitch,
setProxyStringRaw: (newValue) => {
const oldValue = this.state.proxyStringRaw;
localStorage.setItem(UI_RAW, newValue);
this.setState({proxyStringRaw: newValue});
this.mayEmitNewValue(oldValue, newValue);
},
}, originalProps);
return this.state.ifExportsMode
? createElement(ExportsEditor, props)
: createElement(TabledEditor, props);
};
}
};

View File

@ -0,0 +1,176 @@
import Inferno, { linkEvent } from 'inferno';
import Component from 'inferno-component';
import css from 'csjs-inject';
export default function getTabPannel({ flags, baseCss }) {
const scopedCss = css`
/*.tabContainer {
padding: 0;
}*/
.tabContainer li label {
display: inline-block; /* Needed for ::first-letter below. */
}
.tabContainer li label::first-letter {
text-transform: uppercase;
}
:root.ifInsideOptionsPage .tabContainer {
padding: 0.3em 0 0.4em 0;
}
:root.ifInsideOptionsPage nav.mainNav > section:not(:last-child):not([data-key=ownProxies]):not([data-key=mods]) {
border-bottom: 1px solid var(--cr-options-headline);
}
:root.ifInsideOptionsPage .navLabels {
display: none;
}
/* HIDE starts. */
:root:not(.ifInsideOptionsPage) .mainNav input:not(:checked) + section
{
/* Hide, but preclude width resizes. */
height: 0px !important;
line-height: 0px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
border: none !important;
display: block;
visibility: hidden;
transform: scaleY(0) !important;
}
:root:not(.ifInsideOptionsPage) .mainNav input:not(:checked) + section *
{
margin-top: 0 !important;
margin-bottom: 0 !important;
}
/* HIDE ends. */
.navLabels {
background-color: var(--cr-grey-panel);
text-align: center;
min-width: 24em;
}
.navLabels li label {
display: inline-block;
border: 1px solid var(--ribbon-color);
border-radius: 0.2em;
background-color: white;
color: var(--ribbon-color);
padding: 0.2em 0.3em 0.3em 0.2em;
line-height: 0.8em;
margin: 0.1em 0;
}
.navLabels li label:hover {
background-color: var(--blue-bg);
color: white;
}
/* LABELS starts. */
input[name="selectedTabLabel"]:checked + label:not(:hover)
{
background-color: var(--blue-bg);
color: white;
}
/* ★★★★★ */
.navLabels label:before {
content: '★';
padding-right: 0.1em;
visibility: hidden;
}
.navLabels li label:hover:before,
input[name="selectedTabLabel"]:checked + label:before
{
visibility: initial;
}
.navLabels li {
margin: 0 0.125em; /* 1.5px */
}
/* LABELS ends. */
.mainNav {
padding-top: 0.6em;
padding-bottom: 1em;
}
`;
if (flags.ifInsideOptionsPage) {
document.documentElement.classList.add(scopedCss.ifInsideOptionsPage);
}
return class TabPannel extends Component {
constructor(props) {
super(props);
const fromHash = props.hashParams.get('tab');
this.state = {
chosenTabKeyRaw: fromHash,
};
}
render(props) {
const indexedTabs = props.tabs.filter((tab) => tab.label);
let [chosenTabIndex] = indexedTabs
.map((tab, index) => tab.key === this.state.chosenTabKeyRaw ? index : false)
.filter((index) => index !== false);
if (!(chosenTabIndex >= 0)) {
chosenTabIndex = 0;
}
const chosenTabKey = indexedTabs[chosenTabIndex].key;
if (chosenTabKey !== props.hashParams.get('tab')) {
props.hashParams.set('tab', chosenTabKey);
window.location.hash = props.hashParams.toString();
}
return (
<div>
<nav class={scopedCss.navLabels}>
<ul class='horizontalList'>
{
indexedTabs.map((tab, index) =>
(<li>
<input type="radio" name="selectedTabLabel" id={'radioLabel' + index} checked={chosenTabIndex === index} class="off"/>
<label onClick={() => this.setState({chosenTabKeyRaw: tab.key})} for={'radioLabel' + index} class={scopedCss.navLabel}>{tab.label}</label>
</li>)
)
}
</ul>
<hr/>
</nav>
<nav class={'horPadded ' + scopedCss.mainNav}>
{
[].concat(...props.tabs.map((tab, index) => [
(<input type="checkbox" name="selectedTab" id={'radioTab' + index} checked={
chosenTabKey === tab.key || (props.alwaysShownWith[tab.key] || []).includes(chosenTabKey)
} class="off"/>),
(<section id={'tab' + index} class={scopedCss.tabContainer} data-key={tab.key}>{tab.content}</section>),
]))
}
</nav>
<hr/>
</div>);
}
};
};

View File

@ -0,0 +1,152 @@
export default function append(document, { flags }) {
// innerText converts \n to <br>, so:
document.querySelector('style').innerHTML = `
/* GLOBAL VARIABLES */
:root {
--ribbon-color: #4169e1;
--blue-bg: dodgerblue;
--default-grey: #bfbfbf;
--cr-options-headline: #d3d3d3;
--cr-icon-selected: #d7d7d7;
--cr-popup-border: #bababa;
--cr-grey-panel: #f2f2f2;
${ flags.ifInsideOptionsPage ? '' : 'max-width: 24em;' }
}
/* BASE ELEMENTS */
body {
margin: 0;
}
a, a:visited {
color: var(--ribbon-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
label {
user-select: none;
}
div, section, header, ul, ol {
margin: 0;
padding: 0;
}
header {
margin-bottom: 0.3em
}
ul, ol {
list-style-type: none;
}
.nowrap {
white-space: nowrap;
word-break: keep-all;
}
.nowrap {
display: block;
}
.middledChildren > li,
.middledChildren > li > * {
vertical-align: middle;
}
input[type="radio"], input[type="checkbox"] {
flex-shrink: 0;
}
input[type="radio"], label {
cursor: pointer;
}
hr {
border: none;
border-top: 1px solid var(--cr-popup-border);
margin: 0 0 0.6em 0;
padding: 0;
}
em {
font-style: normal;
text-decoration: underline;
}
/* IF INSIDE OPTIONS */
${
flags.ifInsideOptionsPage
? `
ul, ol {
margin-left: 0.4em;
}
` : `
ul, ol {
/*Here is a flex bug:
() antizapret [update] (i)
() anticensority very_long_foobar [update] (i) <- Sic!
Also: options page is wider, check it too.
But: fixed 100% width conflicts with margins/paddings.
So: use only when needed and avoid margins.
FYI: setting left-margin fixes problem too, but margins are not wanted.
Fix this problem below:
*/
display: inline-block;
min-width: 100%;
}
`
}
/* COMMON CLASSES */
.off {
display: none;
}
.horPadded {
padding-left: 1.4em;
padding-right: 1.4em;
}
.horizontalList,
.horizontalList li {
line-height: 100%;
}
.horizontalList li {
display: inline-block;
}
/* Flexes */
.horFlex {
display: flex;
align-items: baseline;
justify-content: space-between;
width: 100%;
}
.horFlex > input:not([type="button"]) {
align-self: flex-end;
}
/* Fonts/Icons */
@font-face {
font-family: "emoji";
src:url("../lib/fonts/emoji.woff") format("woff");
font-weight: normal;
font-style: normal;
}
.emoji {
font-family: "emoji";
}
svg.icon {
display: inline-block;
width: 1em;
height: 1em;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
}
`;
};

View File

@ -0,0 +1,57 @@
// @flow
import Inferno from 'inferno';
import createElement from 'inferno-create-element';
import appendGlobalCss from './globalCss';
import css from 'csjs-inject';
import getApp from './components/App';
chrome.runtime.getBackgroundPage( (bgWindow) =>
bgWindow.apis.errorHandlers.installListenersOn(
window, 'PUP', async() => {
let theState;
{
const apis = bgWindow.apis;
theState = {
utils: bgWindow.utils,
apis: apis,
flags: {
/* Shortcuts to boolean values. */
ifNotControlled: !apis.errorHandlers.ifControllable,
ifMini: apis.version.ifMini,
},
bgWindow,
};
}
// IF INSIDE OPTIONS TAB
const currentTab = await new Promise(
(resolve) => chrome.tabs.query(
{active: true, currentWindow: true},
([tab]) => resolve(tab)
)
);
theState.flags.ifInsideOptionsPage = !currentTab || currentTab.url.startsWith('chrome://extensions/?options=');
theState.currentTab = currentTab;
// STATE DEFINED, COMPOSE.
appendGlobalCss(document, theState);
// Extendable css classes.
Inferno.render(
createElement(getApp(theState), theState),
document.getElementById('app-root'),
);
// READY TO RENDER
document.documentElement.style.display = 'initial';
}
)
);

View File

@ -0,0 +1,49 @@
'use strict';
const path = require('path');
//const BabiliPlugin = require('babili-webpack-plugin');
module.exports = (env, ...flags) => ({
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: `bundle.min.js`,
publicPath: './dist/',
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.jsx?$/,
include: path.join(__dirname, 'src'),
use: [
{
loader: 'babel-loader',
options: {
presets: ['flow'],
plugins: [
'dynamic-import-webpack',
'inferno',
],
},
},
/*{
loader: './lib/transform-loader?csjs-injectify',
},*/
],
}
],
},
resolve: {
aliasFields: [],
},
plugins: env === 'prod' ?
[
/* Production */
//new BabiliPlugin(),
] : [
/* Development */
],
});

View File

@ -0,0 +1,85 @@
'use strict';
const Storage = require('_project-root/tools/sinon-storage');
const Chai = require('chai');
const Mocha = require('mocha');
const CachelessRequire = require('_project-root/tools/cacheless-require')(module);
Mocha.describe('window.apis.pacKitchen', function () {
Mocha.beforeEach(function() {
global.chrome = CachelessRequire('sinon-chrome/extensions');
global.window = {
chrome: global.chrome,
localStorage: new Storage(),
};
CachelessRequire('../00-init-apis.js');
});
Mocha.it('is exported with correct default values', function () {
CachelessRequire('../35-pac-kitchen-api.js');
Chai.expect(window.apis.pacKitchen, 'to be exported as global').to.exist;
const mods = window.apis.pacKitchen.getPacMods();
Chai.expect(
mods, 'to expose default configs on first run'
).to.exist;
Chai.expect(mods.ifNoMods, 'to impose modifications by default (prohibits DIRECT)').to.be.false;
const orderedMods = window.apis.pacKitchen.getOrderedConfigs();
Chai.expect(orderedMods, 'to have method for getting ordered configs').to.exist;
{
const several = 9;
Chai.expect(orderedMods.length, 'to have several ordered configs').to.be.above(several);
}
Chai.expect(
Object.keys(mods).length,
'pacModifiers to inherit default configs keys as props and add extra props'
).to.be.above(orderedMods.length);
Chai.expect(
orderedMods.every((mod) => mods[mod.key] === mod.dflt),
'all configs to be default on first run'
).to.be.ok;
const excMods = window.apis.pacKitchen.getOrderedConfigs('exceptions');
Chai.expect(excMods.length, 'to have several ordered mods under category "exceptions"').to.be.above(0);
const proxyMods = window.apis.pacKitchen.getOrderedConfigs('ownProxies');
Chai.expect(proxyMods.length, 'to have several ordered mods under category "ownProxies"').to.be.above(0);
const generalMods = window.apis.pacKitchen.getOrderedConfigs('general');
Chai.expect(generalMods.length, 'to have several ordered mods without category').to.be.above(0);
Chai.expect(
orderedMods.length, 'to be a sum of categorized (and ordered) mods'
).to.be.equal(
proxyMods.length + excMods.length + generalMods.length
);
});
Mocha.it('is installed (by modifying `chrome.proxy.settings.set`)', function () {
const originalSet = chrome.proxy.settings.set;
CachelessRequire('../35-pac-kitchen-api.js');
Chai.expect(originalSet.notCalled, 'original set not to be called during install').to.be.true;
Chai.expect(originalSet, 'settings.set to be modified during install').not.to.be.equal(chrome.proxy.settings.set);
});
Mocha.afterEach(function() {
delete global.window;
});
});

View File

@ -0,0 +1,35 @@
'use strict';
const Chai = require('chai');
const Mocha = require('mocha');
const CachelessRequire = require('_project-root/tools/cacheless-require')(module);
Mocha.describe('window.utils', function () {
const initApis = '../00-init-apis.js';
Mocha.beforeEach(function() {
global.window = {};
});
Mocha.it('is exported as global', function () {
CachelessRequire(initApis);
Chai.expect(window.utils, 'to be exported as global').to.exist;
Chai.expect(window.apis.version.ifMini, 'to be marked as not MINI version by default').to.be.false;
});
Mocha.afterEach(function() {
delete global.window;
});
});

View File

@ -0,0 +1,32 @@
'use strict';
{
chrome.webNavigation.onErrorOccurred.addListener((details) => {
const tabId = details.tabId;
if ( !(details.frameId === 0 && tabId >= 0) ||
[
'net::ERR_BLOCKED_BY_CLIENT',
'net::ERR_ABORTED',
].includes(details.error) ) {
return;
}
chrome.browserAction.setPopup({
tabId,
popup: './pages/options/index.html#tab=exceptions&status=Правый клик по иконке — меню инструментов!',
});
window.chrome.browserAction.setBadgeBackgroundColor({
tabId,
color: '#4285f4',
});
chrome.browserAction.setBadgeText({
tabId,
text: '●●●',
});
});
}

View File

@ -0,0 +1,55 @@
'use strict';
{
const chromified = window.utils.chromified;
const lastErrors = [];
const lastErrorsLength = 20;
const IF_COLL_KEY = 'err-to-exc-if-coll';
const privates = {
ifCollecting: window.localStorage[IF_COLL_KEY] || false,
};
const that = window.apis.lastNetErrors = {
get ifCollecting() {
return privates.ifCollecting;
},
set ifCollecting(newValue) {
privates.ifCollecting = window.localStorage[IF_COLL_KEY] = newValue;
},
get: () => lastErrors,
}
chrome.webRequest.onErrorOccurred.addListener(chromified((err/*Ignored*/, details) => {
if (!that.ifCollecting || [
'net::ERR_BLOCKED_BY_CLIENT',
'net::ERR_ABORTED',
'net::ERR_CACHE_MISS',
'net::ERR_INSUFFICIENT_RESOURCES',
].includes(details.error) ) {
return;
}
const last = lastErrors[0];
if (last && details.error === last.error && details.url === last.url) {
// Dup.
return;
}
lastErrors.unshift(details);
if (lastErrors.length > lastErrorsLength) {
lastErrors.pop();
}
}),
{urls: ['<all_urls>']}
);
}

View File

@ -18,10 +18,6 @@
const chromified = window.utils.chromified; const chromified = window.utils.chromified;
window.chrome.browserAction.setBadgeBackgroundColor({
color: '#db4b2f',
});
const _tabCallbacks = {}; const _tabCallbacks = {};
const afterTabUpdated = function afterTabUpdated(tabId, cb) { const afterTabUpdated = function afterTabUpdated(tabId, cb) {
@ -45,6 +41,15 @@
chrome.tabs.onUpdated.addListener( onTabUpdate ); chrome.tabs.onUpdated.addListener( onTabUpdate );
const setRedBadge = (opts) => {
window.chrome.browserAction.setBadgeBackgroundColor({
color: '#db4b2f',
});
chrome.browserAction.setBadgeText(opts);
};
const updateTitle = function updateTitle(requestDetails, proxyHost, cb) { const updateTitle = function updateTitle(requestDetails, proxyHost, cb) {
chrome.browserAction.getTitle( chrome.browserAction.getTitle(
@ -71,7 +76,7 @@
+ proxyTitle + '\n' + indent + proxyHost; + proxyTitle + '\n' + indent + proxyHost;
ifShouldUpdateTitle = true; ifShouldUpdateTitle = true;
chrome.browserAction.setBadgeText({ setRedBadge({
tabId: requestDetails.tabId, tabId: requestDetails.tabId,
text: requestDetails.type === 'main_frame' ? '1' : '%1', text: requestDetails.type === 'main_frame' ? '1' : '%1',
}); });
@ -101,13 +106,11 @@
{tabId: requestDetails.tabId}, {tabId: requestDetails.tabId},
(result) => { (result) => {
chrome.browserAction.setBadgeText( setRedBadge({
{
tabId: requestDetails.tabId, tabId: requestDetails.tabId,
text: (isNaN( result.charAt(0)) && result.charAt(0) || '') text: (isNaN( result.charAt(0)) && result.charAt(0) || '')
+ (hostsProxiesPair[0].split('\n').length - 1), + (hostsProxiesPair[0].split('\n').length - 1),
} });
);
return _cb(); return _cb();
} }

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html style="display: none; will-change: contents, display">
<head>
<meta charset="utf-8">
<title>Последние ошибки</title>
<style>
html {
display: flex;
}
body {
display: inline-block;
margin: 0 auto;
}
table {
width: 100%;
border-collapse: collapse;
background-color: #f3f5f6;
margin-bottom: 1em;
}
td, th {
border-style: solid;
border-width: 1px;
padding: 0.5em;
}
a:not(:hover) {
text-decoration: none;
}
tr > td:first-child {
text-align: center;
}
main {
display: block-inline;
margin: 0 auto;
}
h1,h2,h3,h4 {
text-align: center;
}
#addBtn {
float: right;
}
</style>
</head>
<body>
<h3>Список последних ошибок</h3>
Новые сверху, количество ограничено 20тью.
<table>
<thead>
<tr>
<th>Номер</th>
<th>Хост</th>
<th>Ошибка</th>
<th><a href id="allBtn">Все</a></th>
</tr>
</thead>
<tbody id="errorsTable"></tbody>
</table>
<button id="addBtn">Добавить в исключения</button>
<script src="./index.js"></script>
</body>
</html>

View File

@ -0,0 +1,79 @@
'use strict';
chrome.runtime.getBackgroundPage( (bgWindow) =>
bgWindow.apis.errorHandlers.installListenersOn(
window, 'LERR', () => {
const tbody = document.getElementById('errorsTable');
const errors = bgWindow.apis.lastNetErrors.get().map(
({url, error}, index) => ({ message: error, hostname: new URL(url).hostname, ifChecked: false })
);
const renderTbody = () => {
const exc = bgWindow.apis.pacKitchen.getPacMods().exceptions || {};
tbody.innerHTML = '';
if (!errors.length) {
tbody.innerHTML = '<tr><td colspan="4">Ошибок пока не было.</td></tr>';
return;
}
errors.forEach((err, index) => {
const ifProxy = exc[err.hostname];
let style = '';
if (ifProxy !== undefined) {
style = `style="color: ${ifProxy ? 'green' : 'red' }"`;
}
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${index}</td>
<td ${style}>${err.hostname}</td>
<td>${err.message}</td>
<td><input type="checkbox" ${ err.ifChecked ? 'checked' : '' }></td>
`;
tr.querySelector('input').onchange = function() {
errors[index].ifChecked = this.checked;
return false;
};
tbody.appendChild(tr);
});
};
document.getElementById('allBtn').onclick = () => {
const ifAllChecked = errors.every((err) => err.ifChecked);
if (ifAllChecked) {
errors.forEach((err) => { err.ifChecked = false; })
} else {
errors.forEach((err) => { err.ifChecked = true; })
}
renderTbody();
return false;
};
document.getElementById('addBtn').onclick = () => {
const mutatedMods = bgWindow.apis.pacKitchen.getPacMods();
const exc = mutatedMods.exceptions || {};
mutatedMods.exceptions = errors.reduce((acc, err) => {
if (err.ifChecked) {
acc[err.hostname] = true;
}
return acc;
}, exc);
bgWindow.apis.pacKitchen.keepCookedNowAsync(mutatedMods, (err) => alert(err || 'Сделано!'));
};
renderTbody();
document.documentElement.style.display = '';
})
);

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const commonContext = { const commonContext = {
version: '0.34', version: '1.0',
}; };
exports.contexts = {}; exports.contexts = {};
@ -10,10 +10,10 @@ exports.contexts.full = Object.assign({}, commonContext, {
versionSuffix: '', versionSuffix: '',
nameSuffixEn: '', nameSuffixEn: '',
nameSuffixRu: '', nameSuffixRu: '',
extra_permissions: ', "webRequest"', extra_permissions: ', "webRequest", "webNavigation"',
persistent: '', persistent: '',
scripts_2x: ', "20-ip-to-host-api.js"', scripts_2x: ', "20-ip-to-host-api.js"',
scripts_7x: ', "70-block-informer.js"', scripts_8x: ', "80-error-menu.js", "83-last-errors.js", "85-block-informer.js"',
}); });
exports.contexts.mini = Object.assign({}, commonContext, { exports.contexts.mini = Object.assign({}, commonContext, {
@ -23,6 +23,6 @@ exports.contexts.mini = Object.assign({}, commonContext, {
extra_permissions: '', extra_permissions: '',
persistent: '"persistent": false,', persistent: '"persistent": false,',
scripts_2x: ', "20-for-mini-only.js"', scripts_2x: ', "20-for-mini-only.js"',
scripts_7x: '', scripts_8x: '',
}); });

View File

@ -0,0 +1,10 @@
'ust strict';
module.exports = (parentModule) => function cachelessRequire(path) {
for(let key of Object.keys(require.cache)) {
delete require.cache[key];
}
return parentModule.require(path);
};

View File

@ -0,0 +1,48 @@
'use strict';
const Sinon = require('sinon');
module.exports = function storageMock() {
let storage = {};
return new Proxy({
setItem: Sinon.spy(function(key, value) {
storage[key] = value || '';
}),
getItem: Sinon.spy(function(key) {
return key in storage ? storage[key] : null;
}),
removeItem: Sinon.spy(function(key) {
delete storage[key];
}),
get length() {
return Object.keys(storage).length;
},
key: Sinon.spy(function(i) {
throw new Error('Not implemented!');
}),
clear: Sinon.spy(function() {
storage = {};
}),
}, {
get: function(target, name) {
if (name in target) {
return target[name];
}
return target.getItem(name);
},
set: function(target, prop, value) {
if (prop in target) {
target[prop] = value;
return;
}
return target.setItem(prop, value);
},
});
};

File diff suppressed because it is too large Load Diff