diff --git a/README.md b/README.md index 8874c55..d3b2782 100755 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ This repo contains: [WebStore](https://chrome.google.com/webstore/detail/npgcnondjocldhldegnakemclmfkngch) | [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 -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). -[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 diff --git a/extensions/chromium/runet-censorship-bypass/.gitignore b/extensions/chromium/runet-censorship-bypass/.gitignore index c7bfad5..26f3785 100644 --- a/extensions/chromium/runet-censorship-bypass/.gitignore +++ b/extensions/chromium/runet-censorship-bypass/.gitignore @@ -1,4 +1,6 @@ node_modules +node_modules_linux +node_modules_win npm-debug.log -.swp +*.swp build/ diff --git a/extensions/chromium/runet-censorship-bypass/gulpfile.js b/extensions/chromium/runet-censorship-bypass/gulpfile.js index 41da081..0f2ea15 100644 --- a/extensions/chromium/runet-censorship-bypass/gulpfile.js +++ b/extensions/chromium/runet-censorship-bypass/gulpfile.js @@ -4,6 +4,7 @@ const gulp = require('gulp'); const del = require('del'); const through = require('through2'); const PluginError = require('gulp-util').PluginError; +const changed = require('gulp-changed'); const PluginName = 'Template literals'; @@ -21,7 +22,7 @@ const templatePlugin = (context) => through.obj(function(file, encoding, cb) { const {keys, values} = Object.keys(context).reduce( (acc, key) => { - const value = context[key]; + const value = context[key]; acc.keys.push(key); acc.values.push(value); return acc; @@ -44,39 +45,61 @@ const templatePlugin = (context) => through.obj(function(file, encoding, cb) { 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; -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(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(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(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(gulp.dest('./build/extension-full')); + .pipe(gulp.dest(fullDst)) + .on('end', cb); }); diff --git a/extensions/chromium/runet-censorship-bypass/package.json b/extensions/chromium/runet-censorship-bypass/package.json index e161dd3..5fdf7a3 100644 --- a/extensions/chromium/runet-censorship-bypass/package.json +++ b/extensions/chromium/runet-censorship-bypass/package.json @@ -3,14 +3,21 @@ "version": "0.0.19", "description": "Development tools for chromium extension", "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", - "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", "license": "GPLv3", "devDependencies": { + "chai": "^3.5.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": { "del": "^2.2.2", diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/00-init-apis.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/00-init-apis.js index 1cb6214..fff4a1a 100644 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/00-init-apis.js +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/00-init-apis.js @@ -125,16 +125,16 @@ key = prefix + key; if (value === null) { - return localStorage.removeItem(key); + return window.localStorage.removeItem(key); } if (value === undefined) { - const item = localStorage.getItem(key); + const item = window.localStorage.getItem(key); return item && JSON.parse(item); } if (value instanceof Date) { 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 = { version: { ifMini: false, + build: chrome.runtime.getManifest().version.replace(/\d+\.\d+\./g, ''), }, }; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/11-error-handlers-api.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/11-error-handlers-api.js index b4cbac3..96d3263 100644 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/11-error-handlers-api.js +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/11-error-handlers-api.js @@ -61,7 +61,7 @@ const ifPrefix = 'if-on-'; 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 = { diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/35-pac-kitchen-api.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/35-pac-kitchen-api.js index 758ce70..80ede86 100644 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/35-pac-kitchen-api.js +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/35-pac-kitchen-api.js @@ -12,66 +12,79 @@ const ifIncontinence = 'if-incontinence'; const modsKey = 'mods'; - // Don't keep objects in defaults or at least freeze them! - const configs = { + const getDefaultConfigs = () => ({// Configs user may mutate them and we don't care! ifProxyHttpsUrlsOnly: { dflt: false, label: 'проксировать только HTTPS-сайты', desc: 'Проксировать только сайты, доступные по шифрованному протоколу HTTPS. Прокси и провайдер смогут видеть только адреса проксируемых HTTPS-сайтов, но не их содержимое. Используйте, если вы не доверяете прокси-серверам ваш HTTP-трафик. Разумеется, что с этой опцией разблокировка HTTP-сайтов работать не будет.', - index: 0, + order: 0, }, ifUseSecureProxiesOnly: { dflt: false, label: 'только шифрованная связь с прокси', desc: 'Шифровать соединение до прокси от провайдера, используя только прокси типа HTTPS или локальный Tor. Провайдер всё же сможет видеть адреса (но не содержимое) проксируемых ресурсов из протокола DNS (даже с Tor). Опция вряд ли может быть вам полезна, т.к. шифруется не весь трафик, а лишь разблокируемые ресурсы.', - index: 1, + order: 1, }, ifProhibitDns: { dflt: false, label: 'запретить опредление по IP/DNS', desc: 'Пытается запретить скрипту использовать DNS, без которого определение блокировки по IP работать не будет (т.е. будет разблокироваться меньше сайтов). Используйте, чтобы получить прирост в производительности или если вам кажется, что мы проксируем слишком много сайтов. Запрет действует только для скрипта, браузер и др.программы продолжат использование DNS.', - index: 2, + order: 2, }, ifProxyOrDie: { dflt: true, ifDfltMods: true, label: 'проксируй или умри!', desc: 'Запрещает соединение с сайтами напрямую без прокси в случаях, когда все прокси отказывают. Например, если все ВАШИ прокси вдруг недоступны, то добавленные вручную сайты открываться не будут совсем. Однако смысл опции в том, что она препятствует занесению прокси в чёрные списки Хрома. Рекомендуется не отключать.', - index: 3, + order: 3, }, ifUsePacScriptProxies: { dflt: true, + category: 'ownProxies', label: 'использовать прокси PAC-скрипта', desc: 'Использовать прокси-сервера от авторов PAC-скрипта.', - index: 4, + order: 4, }, ifUseLocalTor: { dflt: false, + category: 'ownProxies', label: 'использовать СВОЙ локальный Tor', desc: 'Установите Tor на свой компьютер и используйте его как прокси-сервер. ВАЖНО', - index: 5, + order: 5, }, exceptions: { + category: 'exceptions', dflt: null, }, ifMindExceptions: { dflt: true, + category: 'exceptions', label: 'учитывать исключения', desc: 'Учитывать сайты, добавленные вручную. Только для своих прокси-серверов! Без своих прокси работать не будет.', - index: 6, + order: 6, }, customProxyStringRaw: { dflt: '', + category: 'ownProxies', label: 'использовать СВОИ прокси', url: 'https://rebrand.ly/ac-own-proxy', - index: 7, + order: 7, + }, + ifProxyMoreDomains: { + ifDisabled: true, + dflt: false, + category: 'ownProxies', + label: 'проксировать .onion, .i2p и OpenNIC', + desc: 'Проксировать особые домены. Необходима поддержка со стороны СВОИХ прокси.', + order: 8, }, - }; + }); const getDefaults = function getDefaults() { + const configs = getDefaultConfigs(); return Object.keys(configs).reduce((acc, key) => { acc[key] = configs[key].dflt; @@ -83,7 +96,15 @@ 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) { throw err; } @@ -91,26 +112,33 @@ }; - const getOrderedConfigsForUser = function getOrderedConfigs() { + const getOrderedConfigsForUser = function getOrderedConfigs(category) { 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]; - if(typeof(conf.index) === 'number') { - arr[conf.index] = conf; - conf.value = pacMods[key]; - conf.key = key; - } - return arr; + const conf = configs[key]; + if(typeof(conf.order) === 'number') { + if(!category || category === (conf.category || 'general')) { + conf.value = pacMods[key]; + conf.key = key; + conf.category = category || 'general'; + arr.push(conf); + } + } + return arr; - }, []); + }, []); }; const createPacModifiers = function createPacModifiers(mods = {}) { mods = mods || {}; // null? + const configs = getDefaultConfigs(); const ifNoMods = Object.keys(configs) .every((dProp) => { @@ -123,7 +151,6 @@ }); - console.log('Input mods:', mods); const self = {}; Object.assign(self, getDefaults(), mods); self.ifNoMods = ifNoMods; @@ -140,7 +167,8 @@ } } 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 = ''; @@ -154,7 +182,18 @@ 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) { self.included = []; self.excluded = []; @@ -175,7 +214,7 @@ }); if (self.included.length && !self.filteredCustomsString) { return [null, self, new TypeError( - 'Имеются сайты, добавленные вручную. Они проксироваться не будут, т.к. нет СВОИХ проски, удовлетворяющих вашим запросам!' + 'Имеются сайты, добавленные вручную. Они проксироваться не будут, т.к. нет СВОИХ проски, удовлетворяющих вашим требованиям! Если прокси всё же имеются, то проверьте требования (модификаторы).' )]; } } @@ -191,105 +230,140 @@ cook(pacData, pacMods = mandatory()) { return pacMods.ifNoMods ? pacData : pacData + `${ kitchenStartsMark } -;+function(global) { - "use strict"; - - const originalFindProxyForURL = FindProxyForURL; - global.FindProxyForURL = function(url, host) { - ${function() { - - let res = pacMods.ifProhibitDns ? ` - global.dnsResolve = function(host) { return null; }; - ` : ''; - if (pacMods.ifProxyHttpsUrlsOnly) { - - res += ` - if (!url.startsWith("https")) { - return "DIRECT"; - } - `; - } - res += ` - const directIfAllowed = ${pacMods.ifProxyOrDie ? '""/* Not allowed. */' : '"; DIRECT"'}; - `; - - const ifIncluded = pacMods.included && pacMods.included.length; - const ifExcluded = pacMods.excluded && pacMods.excluded.length; - const ifExceptions = ifIncluded || ifExcluded; - - if (ifExceptions) { - res += ` - /* EXCEPTIONS START */ - const dotHost = '.' + host; - const isHostInDomain = (domain) => dotHost.endsWith('.' + domain); - const domainReducer = (maxWeight, [domain, ifIncluded]) => { - - if (!isHostInDomain(domain)) { - return maxWeight; - } - const newWeightAbs = domain.length; - if (newWeightAbs < Math.abs(maxWeight)) { - return maxWeight; - } - return newWeightAbs*(ifIncluded ? 1 : -1); - - }; - - const excWeight = ${JSON.stringify(Object.entries(pacMods.exceptions))}.reduce( domainReducer, 0 ); - if (excWeight !== 0) { - if (excWeight > 0) { - // Always proxy it! - ${ pacMods.filteredCustomsString - ? `return "${pacMods.filteredCustomsString}" + directIfAllowed;` - : '/* No proxies -- continue. */' - } - } else { - // Never proxy it! - return "DIRECT"; - } - } - /* EXCEPTIONS END */ -`; - } - res += ` - const pacProxyString = originalFindProxyForURL(url, host)${ - pacMods.ifProxyOrDie ? '.replace(/DIRECT/g, "")' : ' + directIfAllowed' - };`; - if( - !pacMods.ifUseSecureProxiesOnly && - !pacMods.filteredCustomsString && - pacMods.ifUsePacScriptProxies - ) { - return res + ` - return pacProxyString;`; - } - - return res + ` - let pacProxyArray = pacProxyString.split(/(?:\\s*;\\s*)+/g).filter( (p) => p ); - const ifNoProxies = pacProxyArray${pacMods.ifProxyOrDie ? '.length === 0' : '.every( (p) => /^DIRECT$/i.test(p) )'}; - if (ifNoProxies) { - // Directs only or null, no proxies. - return "DIRECT"; - } - return ` + +/******/ +/******/;+function(global) { +/******/ "use strict"; +/******/ +/******/ const originalFindProxyForURL = FindProxyForURL; +/******/ global.FindProxyForURL = function(url, host) { +/******/ + ${ function() { - if (!pacMods.ifUsePacScriptProxies) { - return `"${pacMods.filteredCustomsString}"`; - } - let filteredPacExp = 'pacProxyString'; - if (pacMods.ifUseSecureProxiesOnly) { - filteredPacExp = - 'pacProxyArray.filter( (pStr) => /^HTTPS\\s/.test(pStr) ).join("; ")'; - } - if ( !pacMods.filteredCustomsString ) { - return filteredPacExp; - } - return `${filteredPacExp} + "; ${pacMods.filteredCustomsString}"`; + let res = pacMods.ifProhibitDns ? ` +/******/ +/******/ global.dnsResolve = function(host) { return null; }; +/******/ +/******/` : ''; + if (pacMods.ifProxyHttpsUrlsOnly) { - }() + ' + directIfAllowed;'; // Without DIRECT you will get 'PROXY CONN FAILED' pac-error. + res += ` +/******/ +/******/ if (!url.startsWith("https")) { +/******/ return "DIRECT"; +/******/ } +/******/ +/******/ `; + } + if (pacMods.ifUseLocalTor) { - }()} + res += ` +/******/ +/******/ if (host.endsWith(".onion")) { +/******/ return "${pacMods.torPoints.join('; ')}"; +/******/ } +/******/ +/******/ `; + } + res += ` +/******/ +/******/ const directIfAllowed = ${pacMods.ifProxyOrDie ? '""/* Not allowed. */' : '"; DIRECT"'}; +/******/`; + if (pacMods.filteredCustomsString) { + res += ` +/******/ +/******/ const filteredCustomProxies = "; ${pacMods.filteredCustomsString}"; +/******/`; + } + + const ifIncluded = pacMods.included && pacMods.included.length; + const ifExcluded = pacMods.excluded && pacMods.excluded.length; + 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) { + res += ` +/******/ +/******/ /* EXCEPTIONS START */ +/******/ const dotHost = '.' + host; +/******/ const isHostInDomain = (domain) => dotHost.endsWith('.' + domain); +/******/ const domainReducer = (maxWeight, [domain, ifIncluded]) => { +/******/ +/******/ if (!isHostInDomain(domain)) { +/******/ return maxWeight; +/******/ } +/******/ const newWeightAbs = domain.length; +/******/ if (newWeightAbs < Math.abs(maxWeight)) { +/******/ return maxWeight; +/******/ } +/******/ return newWeightAbs*(ifIncluded ? 1 : -1); +/******/ +/******/ }; +/******/ +/******/ const excWeight = ${ JSON.stringify(Object.entries(finalExceptions)) }.reduce( domainReducer, 0 ); +/******/ if (excWeight !== 0) { +/******/ if (excWeight < 0) { +/******/ // Never proxy it! +/******/ return "DIRECT"; +/******/ } +/******/ // Always proxy it! +${ pacMods.filteredCustomsString + ? `/******/ return filteredCustomProxies + directIfAllowed;` + : '/******/ /* No custom proxies -- continue. */' +} +/******/ } +/******/ /* EXCEPTIONS END */ +`; + } + res += ` +/******/ const pacScriptProxies = originalFindProxyForURL(url, host)${ +/******/ pacMods.ifProxyOrDie ? '.replace(/DIRECT/g, "")' : ' + directIfAllowed' + };`; + if( + !pacMods.ifUseSecureProxiesOnly && + !pacMods.filteredCustomsString && + pacMods.ifUsePacScriptProxies + ) { + return res + ` +/******/ return pacScriptProxies + directIfAllowed;`; + } + + return res + ` +/******/ let pacProxyArray = pacScriptProxies.split(/(?:\\s*;\\s*)+/g).filter( (p) => p ); +/******/ const ifNoProxies = pacProxyArray${pacMods.ifProxyOrDie ? '.length === 0' : '.every( (p) => /^DIRECT$/i.test(p) )'}; +/******/ if (ifNoProxies) { +/******/ // Directs only or null, no proxies. +/******/ return "DIRECT"; +/******/ } +/******/ return ` + + function() { + + if (!pacMods.ifUsePacScriptProxies) { + return ''; + } + let filteredPacExp = 'pacScriptProxies'; + if (pacMods.ifUseSecureProxiesOnly) { + filteredPacExp = + 'pacProxyArray.filter( (pStr) => /^HTTPS\\s/.test(pStr) ).join("; ")'; + } + return filteredPacExp + ' + '; + + }() + `${pacMods.filteredCustomsString ? 'filteredCustomProxies + ' : ''}directIfAllowed;`; // Without DIRECT you will get 'PROXY CONN FAILED' pac-error. + + }() + } }; @@ -360,15 +434,12 @@ const oldProxies = getCurrentConfigs().filteredCustomsString || ''; const newProxies = pacMods.filteredCustomsString || ''; ifProxiesChanged = oldProxies !== newProxies; - console.log('Proxies changed from:', oldProxies, 'to', newProxies); kitchenState(modsKey, pacMods); } - console.log('Keep cooked now...', pacMods); this.setNowAsync( (err, res, ...setWarns) => { const accWarns = modsWarns.concat(setWarns); // Acc = accumulated. - console.log('Try now err:', err); if (err) { return cb(err, res, ...accWarns); } diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/37-sync-pac-script-with-pac-provider-api.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/37-sync-pac-script-with-pac-provider-api.js index 8679370..5706775 100644 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/37-sync-pac-script-with-pac-provider-api.js +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/37-sync-pac-script-with-pac-provider-api.js @@ -169,19 +169,19 @@ pacProviders: { Антизапрет: { label: 'Антизапрет', - desc: 'Альтернативный PAC-скрипт от стороннего разработчика.' + - ' Блокировка определяется по доменному имени,' + - ' для некоторых провайдеров есть автоопредление.' + - '
Страница проекта.', + desc: `Альтернативный PAC-скрипт от стороннего разработчика. + Работает быстрее, но охватывает меньше сайтов. + Блокировка определяется по доменному имени, +
Страница проекта.`, order: 0, pacUrls: ['https://antizapret.prostovpn.org/proxy.pac'], }, Антицензорити: { - label: 'Антицензорити (тормозит)', - desc: 'Основной PAC-скрипт от автора расширения.' + - ' Блокировка определятся по доменному имени или IP адресу.' + - ' Работает на switch-ах.
' + - ' Страница проекта.', + label: 'Антицензорити', + desc: `Основной PAC-скрипт от автора расширения. + Работает медленней, но охватывает больше сайтов. + Блокировка определятся по доменному имени или IP адресу.
+ Страница проекта.`, order: 1, /* @@ -193,13 +193,16 @@ // First official, shortened: 'https://rebrand.ly/ac-chrome-anticensority-pac', // 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', - // GitHub.io: - '\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 - // 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 + // Google Drive (0.17, anticensority): + '\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', + ], }, onlyOwnSites: { label: 'Только свои сайты и свои прокси', @@ -213,7 +216,7 @@ 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, 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. getLastModifiedForKey(key = mandatory()) { @@ -331,6 +344,7 @@ this.lastPacUpdateStamp = Date.now(); this.ifFirstInstall = false; this.setAlarms(); + this.setTitle(); } resolve([err, null, ...warns]); @@ -509,10 +523,10 @@ 2. We have to check storage for migration before using it. Better on each launch then on each pull. */ - const ifUpdating = antiCensorRu.version !== oldStorage.version; await new Promise((resolve) => { + const ifUpdating = antiCensorRu.version !== oldStorage.version; if (!ifUpdating) { // LAUNCH, RELOAD, ENABLE @@ -527,12 +541,7 @@ const key = antiCensorRu._currentPacProviderKey; if (key !== null) { const ifVeryOld = !Object.keys(antiCensorRu.pacProviders).includes(key); - const ifNeedsForcing = (oldStorage.version < '0.0.0.2') && !localStorage.getItem('provider-backup'); - if ( ifVeryOld || ifNeedsForcing ) { - if (ifNeedsForcing) { - console.log('Update forces antizapret...') - localStorage.setItem('provider-backup', antiCensorRu._currentPacProviderKey); - } + if (ifVeryOld) { antiCensorRu._currentPacProviderKey = 'Антизапрет'; } } @@ -549,6 +558,7 @@ if (antiCensorRu.getPacProvider()) { antiCensorRu.setAlarms(); } + antiCensorRu.setTitle(); /* History of Changes to Storage (Migration Guide) diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/70-menu-items.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/70-menu-items.js new file mode 100644 index 0000000..6008e50 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/70-menu-items.js @@ -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); + + }, + + }; + +} diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/75-context-menus.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/75-context-menus.js new file mode 100644 index 0000000..7509c7c --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/75-context-menus.js @@ -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) + ); + + }); + +} diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/80-context-menus.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/80-context-menus.js deleted file mode 100644 index 66e53e5..0000000 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/80-context-menus.js +++ /dev/null @@ -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,Запрашиваю... -
- -
- - - - - - info - - - - - loop-round - - - - - import-export - - - - - - - - - - diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/choose-pac-provider/index.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/choose-pac-provider/index.js deleted file mode 100644 index acd2c30..0000000 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/choose-pac-provider/index.js +++ /dev/null @@ -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('
'); - - let message = ''; - if (err) { - let wrapped = err.wrapped; - message = err.message || ''; - - while( wrapped ) { - const deeperMsg = wrapped && wrapped.message; - if (deeperMsg) { - message = message + ' > ' + deeperMsg; - } - wrapped = wrapped.wrapped; - } - } - message = message.trim(); - if (warning) { - message = message ? message + '
' + warning : warning; - } - setStatusTo( - ` - ${err ? '🔥 Ошибка!' : 'Некритичная ошибка.'} - -
- ${message} - ${err ? '[Техн.детали]' : ''}` - ); - 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 = ` - - - `; - - const infoSign = function infoSign(tooltip) { - - return `
- ${infoIcon} -
${tooltip}
-
`; - - }; - - { - 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 = ` - -
- -  [обновить] -
` + - 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 = ` - -
- -
`; - - if (!ifMultiline) { - li.innerHTML += infoSign(conf.desc); - } else { - li.style.flexWrap = 'wrap'; - li.innerHTML += `${infoIcon} -`; - 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( - 'Неверный формат своих прокси. Свертесь с документацией.' - )); - } - 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 = ` - - `; - 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); - - }) -); diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/exceptions/index.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/exceptions/index.js index 1b514f2..c673243 100644 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/exceptions/index.js +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/exceptions/index.js @@ -16,7 +16,8 @@ chrome.runtime.getBackgroundPage( (backgroundPage) => # Комментарии НЕ сохраняются! # Сначала идёт список проксируемых сайтов, # затем ==== на отдельной строке, -# затем исключённые сайты, отсортированные с конца строки. +# затем исключённые сайты. +# Сортировка — с конца строки. # ПРОКСИРОВАТЬ: diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/lib/chrome-style/check.png b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/lib/chrome-style/check.png new file mode 100644 index 0000000..94c58b7 Binary files /dev/null and b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/lib/chrome-style/check.png differ diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/lib/chrome-style/index.css b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/lib/chrome-style/index.css new file mode 100644 index 0000000..a3cc382 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/lib/chrome-style/index.css @@ -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; +/* + /* 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; +/**/ +} + +:-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; + + /* For better alignment between adjacent buttons and inputs. */ + padding-bottom: 4px; + +} + +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: + * + *
+ * + *
+ */ + +:-webkit-any(.checkbox, .radio) label { + /* Don't expand horizontally: . */ + 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; +} diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/lib/links.txt b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/lib/links.txt new file mode 100644 index 0000000..63869b9 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/lib/links.txt @@ -0,0 +1 @@ +ChromeStyle: https://cs.chromium.org/chromium/src/extensions/renderer/resources/extension.css diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/.flowconfig b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/.flowconfig new file mode 100644 index 0000000..78f533f --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/.flowconfig @@ -0,0 +1,8 @@ +[ignore] +.*/test/.* + +[include] + +[libs] + +[options] diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/.gitignore b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/.gitignore new file mode 100644 index 0000000..b7b476e --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/.gitignore @@ -0,0 +1,4 @@ +node_modules +npm-debug.log +*.swp +dist diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/README.md b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/README.md new file mode 100644 index 0000000..43a029a --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/README.md @@ -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` diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/index.html b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/index.html new file mode 100644 index 0000000..a19cb9a --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/index.html @@ -0,0 +1,39 @@ + + + + + Настройки + + + + +
+ + + info + + + + + loop-round + + + + + import-export + + + + + + + + + + + + diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/lib/transform-loader.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/lib/transform-loader.js new file mode 100644 index 0000000..9b5ccba --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/lib/transform-loader.js @@ -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()) )); + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/package.json b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/package.json new file mode 100644 index 0000000..6d4f4e1 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/package.json @@ -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" + } +} diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/App.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/App.js new file mode 100644 index 0000000..4bfd555 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/App.js @@ -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( +
    + {newsArr.map(([title, url]) => (
  1. {title}
  2. ))} +
+ ); + + } + + 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 + ' > ' + 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('
'); + + messageHtml = messageHtml.trim(); + if (warningHtml) { + messageHtml = messageHtml ? messageHtml + '
' + warningHtml : warningHtml; + } + this.setStatusTo( + ( + + {err ? 🔥 Ошибка! : 'Некритичная oшибка.'} + +
+ + {' '} + {err && { + + this.props.apis.errorHandlers.viewError('pup-ext-err', err); + evt.preventDefault(); + + }}>[Техн.детали]} +
), + 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)), + ]); + + } + + } + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ApplyMods.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ApplyMods.js new file mode 100644 index 0000000..94d8b03 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ApplyMods.js @@ -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 ( +
+ + К изначальным! +
+ ); + + }; + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ExcEditor.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ExcEditor.js new file mode 100644 index 0000000..378e92b --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ExcEditor.js @@ -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 ( +
+
Проксировать указанный сайт?
+
+
+ *. { this.rawInput = inputNode; }} + onKeyDown={this.handleKeyDown.bind(this)} + onInput={this.handleInputOrClick} + onClick={this.handleInputOrClick} + /> +
+ {/**/} + + +
+ + { + 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 +
    +
  1. {' '} + +
  2. +
  3. + + {' '} +
  4. +
  5. + + {' '}
  6. +
+
+ ); + + } + + }; + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Exceptions.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Exceptions.js new file mode 100644 index 0000000..ff695a7 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Exceptions.js @@ -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 + ? ( +
+ Редактор исключений доступен только для вкладок. +
) + : + (
+ {createElement(ExcEditor, props)} + +
+ ); + + }; + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Footer.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Footer.js new file mode 100644 index 0000000..433b091 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Footer.js @@ -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 ( +
+
+
{props.status}
+
+ + +
+ ); + + }; + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/InfoLi.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/InfoLi.js new file mode 100644 index 0000000..131ff77 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/InfoLi.js @@ -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 ( + $ + $ + + ); + + }; + + 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 ( +
  • + {createElement('input', inputProps)} +
    + + {props.nodeAfterLabel} +
    + {props.conf.desc + ? ( +
    + +
    +
    ) + : (props.conf.url + ? () + : ( ) // Affects vertical align of flexbox items. + ) + } + {/* props.checked && props.children */} + {props.checked && props.children && (
    {props.children}
    )} +
  • + ); + + }; + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/LastUpdateDate.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/LastUpdateDate.js new file mode 100644 index 0000000..2b2d75c --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/LastUpdateDate.js @@ -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 (
    Обновлялись: { date.text }
    ); + + } + + }; + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Main.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Main.js new file mode 100644 index 0000000..8ae6249 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Main.js @@ -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'], + }, + })); + + } + + } + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ModList.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ModList.js new file mode 100644 index 0000000..d43c1c1 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ModList.js @@ -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 ( +
      + { + 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 ( this.handleCheck(confMeta, event.target.checked)} + ifInputsDisabled={props.ifInputsDisabled} + > + {child} + ); + + }) + } +
    + ); + + } + + } + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/NotControlledWarning.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/NotControlledWarning.js new file mode 100644 index 0000000..47b9e14 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/NotControlledWarning.js @@ -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 ( +
    + ); + + } + +} diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Notifications.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Notifications.js new file mode 100644 index 0000000..75cd8d6 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Notifications.js @@ -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 ( +
    +
    Я yведомления:
    +
      + { + Array.from(props.apis.errorHandlers.getEventsMap()).map(([ntfId, ntfName]) => { + + const iddy = `if-on-${ntfId}`; + const ifChecked = props.apis.errorHandlers.isOn(ntfId); + return ( +
    • + { + + props.apis.errorHandlers.switch( + ifChecked ? 'off' : 'on', // Reverse. + ntfId + ); + + }} + /> + {' '} + +
    • + ); + + }) + } +
    +
    + ); + + }; + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/PacChooser.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/PacChooser.js new file mode 100644 index 0000000..c5b45b3 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/PacChooser.js @@ -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 ( +
    + {props.flags.ifInsideOptionsPage && (
    PAC-скрипт:
    )} +
      + { + [...props.apis.antiCensorRu.getSortedEntriesForProviders(), {key: 'none', label: 'Отключить'}].map((provConf) => + ([обновить]} + />) + ) + } +
    +
    + { createElement(LastUpdateDate, props) } +
    + { + props.flags.ifMini + ? (🏋) + : (🐌) + } +
    +
    +
    + ); + + } + + componentDidMount() { + + if (this.props.apis.antiCensorRu.ifFirstInstall) { + this.updatePac(); + } + + } + + }; + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ProxyEditor.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ProxyEditor.js new file mode 100644 index 0000000..759aab7 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ProxyEditor.js @@ -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) => + ( + + ); + + 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 ( +
    + + + + + + + + + + + {/* ADD NEW PROXY STARTS. */} + + + + + + + {/* ADD NEW PROXY ENDS. */} + { + splitBySemi(this.props.proxyStringRaw).map((proxyAsString, index) => { + + const [type, addr] = proxyAsString.trim().split(/\s+/); + const [hostname, port] = addr.split(':'); + return ( + + + + + + + + ); + + }) + } + +
    протоколдомен / IPпорт + +
    + + + {/* LAST-2: HOSTNAME */} + + + {/* LAST-1: PORT */} + + + {/* LAST: ADD BUTTON */} + +
    + + {type}{port} + +
    +
    + ); + } + } + + 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 ( +
    + + + + + + + + + +
    + { + this.state.stashedExports === false + ? 'Комментарии вырезаются!' + : (this.state.ifHasErrors + ? (Сбросьте изменения или поправьте) + : (Сбросить изменения) + ) + } + + +