diff --git a/extensions/chromium/runet-censorship-bypass/.gitignore b/extensions/chromium/runet-censorship-bypass/.gitignore index b4cc6d0..26f3785 100644 --- a/extensions/chromium/runet-censorship-bypass/.gitignore +++ b/extensions/chromium/runet-censorship-bypass/.gitignore @@ -2,5 +2,5 @@ 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 67faeb2..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'; @@ -44,9 +45,10 @@ 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(); }); @@ -56,31 +58,48 @@ const excFolder = (name) => [`!./src/**/${name}`, `!./src/**/${name}/**/*`]; const excluded = [ ...excFolder('test') , ...excFolder('node_modules'), ...excFolder('src') ]; const commonWoTests = ['./src/extension-common/**/*', ...excluded]; -gulp.task('_cp-common', ['clean'], function() { +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(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/**/*', ...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/**/*', ...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 3b69739..531cee1 100644 --- a/extensions/chromium/runet-censorship-bypass/package.json +++ b/extensions/chromium/runet-censorship-bypass/package.json @@ -14,6 +14,7 @@ "chai": "^3.5.0", "eslint": "^3.15.0", "eslint-config-google": "^0.7.1", + "gulp-changed": "^3.1.0", "mocha": "^3.3.0", "sinon-chrome": "^2.2.1" }, 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 214d16a..f77aa9f 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,8 +12,7 @@ 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, @@ -75,9 +74,9 @@ ifProxyMoreDomains: { ifDisabled: true, dflt: false, - category: 'ownProxies', + category: 'exceptions', label: 'проксировать .onion, .i2p и OpenNIC', - desc: 'Проксировать особые домены. Необходима поддержка со стороны прокси.', + desc: 'Проксировать особые домены. Необходима поддержка со стороны СВОИХ прокси.', order: 8, }, ifProxyErrors: { @@ -89,10 +88,11 @@ order: 9, }, - }; + }); const getDefaults = function getDefaults() { + const configs = getDefaultConfigs(); return Object.keys(configs).reduce((acc, key) => { acc[key] = configs[key].dflt; @@ -105,13 +105,14 @@ const getCurrentConfigs = function getCurrentConfigs() { const oldMods = kitchenState(modsKey); - if (oldMods) { + /*if (oldMods) { // No migration! return oldMods; - } + }*/ - // In case of first install. - const [err, mods, ...warns] = createPacModifiers(); + // 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; } @@ -122,6 +123,7 @@ const getOrderedConfigsForUser = function getOrderedConfigs(category) { const pacMods = getCurrentConfigs(); + const configs = getDefaultConfigs(); return Object.keys(configs) .sort((keyA, keyB) => configs[keyA].order - configs[keyB].order) .reduce((arr, key) => { @@ -143,6 +145,7 @@ const createPacModifiers = function createPacModifiers(mods = {}) { mods = mods || {}; // null? + const configs = getDefaultConfigs(); const ifNoMods = Object.keys(configs) .every((dProp) => { @@ -171,7 +174,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 = ''; @@ -185,7 +189,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 = []; @@ -222,105 +237,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"'};`; - if (pacMods.filteredCustomsString) { - res += ` - const filteredCustomProxies = "; ${pacMods.filteredCustomsString}";`; - } - - 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) { - // 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(global) { +/******/ "use strict"; +/******/ +/******/ const originalFindProxyForURL = FindProxyForURL; +/******/ global.FindProxyForURL = function(url, host) { +/******/ + ${ function() { - if (!pacMods.ifUsePacScriptProxies) { - return ''; - } - let filteredPacExp = 'pacScriptProxies'; - if (pacMods.ifUseSecureProxiesOnly) { - filteredPacExp = - 'pacProxyArray.filter( (pStr) => /^HTTPS\\s/.test(pStr) ).join("; ")'; - } - return filteredPacExp + ' + '; + let res = pacMods.ifProhibitDns ? ` +/******/ +/******/ global.dnsResolve = function(host) { return null; }; +/******/ +/******/` : ''; + if (pacMods.ifProxyHttpsUrlsOnly) { - }() + `${pacMods.filteredCustomsString ? 'filteredCustomProxies + ' : ''}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 || ifManualExceptions) { + 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. + + }() + } }; 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 fe0d5b0..2775f08 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 @@ -177,7 +177,7 @@ pacUrls: ['https://antizapret.prostovpn.org/proxy.pac'], }, Антицензорити: { - label: 'Антицензорити (тормозит)', + label: 'Антицензорити', desc: 'Основной PAC-скрипт от автора расширения.' + ' Блокировка определятся по доменному имени или IP адресу.' + ' Работает на switch-ах.
' + diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/icons/default-grayscale-128.png b/extensions/chromium/runet-censorship-bypass/src/extension-common/icons/default-grayscale-128.png index 277056b..eeadc19 100644 Binary files a/extensions/chromium/runet-censorship-bypass/src/extension-common/icons/default-grayscale-128.png and b/extensions/chromium/runet-censorship-bypass/src/extension-common/icons/default-grayscale-128.png differ 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 index 2966787..b7b476e 100644 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/.gitignore +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/.gitignore @@ -1,4 +1,4 @@ node_modules npm-debug.log -.swp +*.swp dist 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 index a1d1577..1dccd73 100644 --- 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 @@ -3,594 +3,22 @@ Настройки - + -
- - - - - - - - - - - -
- -
-
-
Загрузка...
-
- - -
- - - - +
- + info - + loop-round - + import-export @@ -604,5 +32,7 @@ + + 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/index.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/old/index.js similarity index 100% rename from extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/index.js rename to extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/old/index.js diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/old/old.html b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/old/old.html new file mode 100644 index 0000000..a1d1577 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/old/old.html @@ -0,0 +1,608 @@ + + + + + Настройки + + + +
+ + + + + + + + + + + +
+ +
+
+
Загрузка...
+
+ + +
+ + + + + + + info + + + + + loop-round + + + + + import-export + + + + + + + + + + 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 index 67c0cd8..a939dbe 100644 --- 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 @@ -6,19 +6,27 @@ "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", "babili-webpack-plugin": "^0.0.11", + "concat-stream": "^1.6.0", + "csjs-inject": "^1.0.1", "flow-bin": "^0.45.0", - "react": "^15.5.4", - "react-dom": "^15.5.4", + "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": "webpack --define process.env.NODE_ENV=\"'production'\" --env=dev", - "build:dev": "webpack --debug --devtool source-map --output-pathinfo --env=dev", - "build": "npm run build:dev" + "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", + "start": "npm run build:dev && npm run gulp" + }, + "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..44556c7 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/App.js @@ -0,0 +1,146 @@ +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); + this.state = { + status: 'Загрузка...', + ifInputsDisabled: false, + }; + + } + + setStatusTo(msg, cb) { + + this.setState( + { + status: msg, + }, + cb + ); + + } + + showErrors(err, ...args/* ...warns, cb */) { + + const lastArg = args[args.length - 1]; + const cb = (lastArg && typeof lastArg === 'function') + ? args.pop() + : () => {}; + const warns = args; + + const warningHtml = warns + .map( + (w) => w && w.message || '' + ) + .filter( (m) => m ) + .map( (m) => '✘ ' + m ) + .join('
'); + + let messageHtml = ''; + if (err) { + let wrapped = err.wrapped; + messageHtml = err.message || ''; + + while( wrapped ) { + const deeperMsg = wrapped && wrapped.message; + if (deeperMsg) { + messageHtml = messageHtml + ' > ' + deeperMsg; + } + wrapped = wrapped.wrapped; + } + } + 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.bind(this), + conduct: this.conduct.bind(this), + showErrors: this.showErrors.bind(this), + }, + ifInputsDisabled: this.state.ifInputsDisabled, + }); + + 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..244e214 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ApplyMods.js @@ -0,0 +1,36 @@ +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.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..47228c2 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ExcEditor.js @@ -0,0 +1,348 @@ +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: {} + }; + + } + + 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); + + const onradio = this.handleRadioClick.bind(this); + const oninput = this.handleInputOrClick.bind(this); + + return ( +
+
Проксировать указанный сайт?
+
+
+ *. { this.rawInput = inputNode; }} + onKeyDown={this.handleKeyDown.bind(this)} + onInput={oninput} + onClick={oninput} + /> +
+ {/**/} + + +
+ + { + 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..c97d77d --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Exceptions.js @@ -0,0 +1,71 @@ +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..ebb12d8 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/InfoLi.js @@ -0,0 +1,153 @@ +import Inferno from 'inferno'; +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; + flex-wrap: wrap; + } + .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 */ + + input:not(:checked) ~ .children { + display: none; + } + .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(props) { + + props = Object.assign({}, { + idPrefix: '', + ifDashify: false, + }, props); + + const iddy = props.idPrefix + ( props.ifDashify ? camelToDash(props.conf.key) : props.conf.key ); + return ( +
  • + +
    + +  {props.nodeAfterLabel} +
    + {props.conf.desc + ? ( +
    + +
    +
    ) + : (props.conf.url + ? () + : ( ) // Affects vertical align of flexbox items. + ) + } + {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..3e6c1a7 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Main.js @@ -0,0 +1,169 @@ +import Inferno, {linkEvent} from 'inferno'; +import Component from 'inferno-component'; +import createElement from 'inferno-create-element'; + +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 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 addChecks = (arr) => arr.map( (conf) => Object.assign(conf, {ifChecked: Boolean(conf.value)}) ); + + 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; + + }); + 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, { + 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..c0aaa4b --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ModList.js @@ -0,0 +1,82 @@ +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.state.checks[confMeta.index] = ifChecked; + if (ifChecked === false || !confMeta.ifChild) { + this.handleNewValue(confMeta, ifChecked); + } else { + this.setState({ + checks: this.state.checks.map( + (ch, i) => i === confMeta.index ? ifChecked : ch + ) + }); + } + + } + + 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)} + > + {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..5b70bd6 --- /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ведомления:
    + +
    + ); + + }; + +}; 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..1dcc388 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/PacChooser.js @@ -0,0 +1,151 @@ +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; + } + 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', + }; + + } + + getCurrentProviderId() { + + return this.props.apis.antiCensorRu.getCurrentPacProviderKey() || 'none'; + + } + + 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 updatePac = function updatePac() { + props.funs.conduct( + 'Обновляем...', + (cb) => props.apis.antiCensorRu.syncWithPacProviderAsync(cb), + 'Обновлено.' + ); + }; + + const iddyToCheck = this.getCurrentProviderId(); + return ( +
    + {props.flags.ifInsideOptionsPage && (
    PAC-скрипт:
    )} + +
    + { createElement(LastUpdateDate, props) } +
    + { + props.flags.ifMini + ? (🏋) + : (🐌) + } +
    +
    +
    + ); + + } + + }; + +}; 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..73b82e6 --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ProxyEditor.js @@ -0,0 +1,540 @@ +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; + } + + table.editor ::-webkit-input-placeholder { + color: #c9c9c9; + } + + table.editor td, table.editor th { + border: 1px solid #ccc; + text-align: left; + height: 100%; + } + + /* ADD PANEL */ + table.editor tr.addPanel td { + padding: 0; + } + /* PROXY ROW */ + table.editor tr.proxyRow td:nth-child(2) { + text-align: center; + } + + 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 { + min-width: 0; + min-height: 0; + 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), + table.editor tr.proxyRow > td:first-child { + 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; + } + + 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.hostname; + const port = elements.port; + + that.props.setProxyStringRaw( + `${that.props.proxyStringRaw} ${type} ${hostname}:${port};`.trim() + ); + + } + + 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}{hostname}{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 + ? (Сбросьте изменения или поправьте) + : (Сбросить изменения) + ) + } + + +