From 361ef9cb742d98988afc0c974f5f2abc2fff609d Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 26 Jun 2016 00:26:12 +0800 Subject: [PATCH] support validate option value before submitting to aria2 --- src/langs/zh_CN.json | 5 + src/scripts/config/aria2Options.js | 292 ++++++++++++++++++-------- src/scripts/config/constants.js | 3 +- src/scripts/config/defaultLanguage.js | 5 + src/scripts/directives/setting.js | 84 ++++++-- 5 files changed, 286 insertions(+), 103 deletions(-) diff --git a/src/langs/zh_CN.json b/src/langs/zh_CN.json index a3213d2..6b2c70d 100644 --- a/src/langs/zh_CN.json +++ b/src/langs/zh_CN.json @@ -120,6 +120,11 @@ "Changes to the settings take effect after refreshing page.": "设置将在页面刷新后生效.", "Type is illegal!": "类型错误!", "Parameter is invalid!": "请求参数无效", + "Option value cannot be empty!": "参数内容不能为空!", + "Input number is invalid!": "输入的数字无效!", + "Input number is below min value!": "输入的数字小于最小值 {{value}} !", + "Input number is above max value!": "输入的数字大于最大值 {{value}} !", + "Input value is invalid!": "输入的内容无效!", "format": { "longdate": "YYYY年MM月DD日 HH:mm:ss", "time.millisecond": "{{value}} 毫秒", diff --git a/src/scripts/config/aria2Options.js b/src/scripts/config/aria2Options.js index 4029267..29fc515 100644 --- a/src/scripts/config/aria2Options.js +++ b/src/scripts/config/aria2Options.js @@ -1,22 +1,41 @@ (function () { 'use strict'; angular.module('ariaNg').constant('aria2AllOptions', { + // EXAMPLE: + // 'option name': { + // type: 'string|integer|float|text|boolean|option', + // [suffix: 'Bytes|Milliseconds|Seconds|Minutes|Hours',] + // [defaultValue: '',] + // [readonly: true|false,] //default: false + // [split: '',] //SUPPORT 'text' type + // [showCount: true|false,] //SUPPORT 'text' type, parameter 'split' is required, default: false + // [options: [],] //SUPPORT 'option' type + // [min: 0,] //SUPPORT 'integer', 'float' + // [max: 0,] //SUPPORT 'integer', 'float' + // [pattern: ''] + // } 'dir': { - type: 'string' + type: 'string', + required: true }, 'log': { - type: 'string' + type: 'string', + required: true }, 'max-concurrent-downloads': { type: 'integer', - defaultValue: '5' + defaultValue: '5', + required: true, + min: 1 }, 'check-integrity': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'continue': { - type: 'boolean' + type: 'boolean', + required: true }, 'all-proxy': { type: 'string' @@ -30,33 +49,48 @@ 'connect-timeout': { type: 'integer', suffix: 'Seconds', - defaultValue: '60' + defaultValue: '60', + required: true, + min: 1, + max: 600 }, 'dry-run': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'lowest-speed-limit': { type: 'string', suffix: 'Bytes', - defaultValue: '0' + defaultValue: '0', + required: true, + pattern: '^(0|[1-9]\\d*(K|M)?)$' }, 'max-connection-per-server': { type: 'integer', - defaultValue: '1' + defaultValue: '1', + required: true, + min: 1, + max: 16 }, 'max-file-not-found': { type: 'integer', - defaultValue: '0' + defaultValue: '0', + required: true, + min: 0 }, 'max-tries': { type: 'integer', - defaultValue: '5' + defaultValue: '5', + required: true, + min: 0 }, 'min-split-size': { type: 'string', suffix: 'Bytes', - defaultValue: '20M' + defaultValue: '20M', + required: true, + pattern: '^(0|[1-9]\\d*(K|M)?)$' }, 'netrc-path': { type: 'string', @@ -64,7 +98,8 @@ defaultValue: '$(HOME)/.netrc' }, 'no-netrc': { - type: 'boolean' + type: 'boolean', + required: true }, 'no-proxy': { type: 'text', @@ -74,20 +109,26 @@ 'proxy-method': { type: 'option', options: ['get', 'tunnel'], - defaultValue: 'get' + defaultValue: 'get', + required: true }, 'remote-time': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'reuse-uri': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'retry-wait': { type: 'integer', suffix: 'Seconds', - defaultValue: '0' + defaultValue: '0', + required: true, + min: 0, + max: 600 }, 'server-stat-of': { type: 'string' @@ -100,22 +141,29 @@ }, 'split': { type: 'integer', - defaultValue: '5' + defaultValue: '5', + required: true, + min: 1 }, 'stream-piece-selector': { type: 'option', options: ['default', 'inorder', 'random', 'geom'], - defaultValue: 'default' + defaultValue: 'default', + required: true }, 'timeout': { type: 'integer', suffix: 'Seconds', - defaultValue: '60' + defaultValue: '60', + required: true, + min: 1, + max: 600 }, 'uri-selector': { type: 'option', options: ['inorder', 'feedback', 'adaptive'], - defaultValue: 'feedback' + defaultValue: 'feedback', + required: true }, 'check-certificate': { type: 'boolean', @@ -124,15 +172,18 @@ }, 'http-accept-gzip': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'http-auth-challenge': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'http-no-cache': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'http-user': { type: 'string' @@ -163,11 +214,13 @@ }, 'enable-http-keep-alive': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'enable-http-pipelining': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'header': { type: 'string' @@ -177,7 +230,8 @@ }, 'use-head': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'user-agent': { type: 'string', @@ -193,7 +247,8 @@ }, 'ftp-pasv': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'ftp-proxy': { type: 'string' @@ -207,11 +262,13 @@ 'ftp-type': { type: 'option', options: ['binary', 'ascii'], - defaultValue: 'binary' + defaultValue: 'binary', + required: true }, 'ftp-reuse-connection': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'ssh-host-key-md': { type: 'string' @@ -227,11 +284,13 @@ }, 'bt-enable-hook-after-hash-check': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'bt-enable-lpd': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'bt-exclude-tracker': { type: 'text', @@ -243,57 +302,73 @@ }, 'bt-force-encryption': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'bt-hash-check-seed': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'bt-max-open-files': { type: 'integer', - defaultValue: '100' + defaultValue: '100', + required: true, + min: 1 }, 'bt-max-peers': { type: 'integer', - defaultValue: '55' + defaultValue: '55', + required: true, + min: 0 }, 'bt-metadata-only': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'bt-min-crypto-level': { type: 'option', options: ['plain', 'arc4'], - defaultValue: 'plain' + defaultValue: 'plain', + required: true }, 'bt-prioritize-piece': { type: 'string' }, 'bt-remove-unselected-file': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'bt-require-crypto': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'bt-request-peer-speed-limit': { type: 'string', suffix: 'Bytes', - defaultValue: '50K' + defaultValue: '50K', + required: true, + pattern: '^(0|[1-9]\\d*(K|M)?)$' }, 'bt-save-metadata': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'bt-seed-unverified': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'bt-stop-timeout': { type: 'integer', suffix: 'Seconds', - defaultValue: '0' + defaultValue: '0', + required: true, + min: 0 }, 'bt-tracker': { type: 'text', @@ -303,17 +378,25 @@ 'bt-tracker-connect-timeout': { type: 'integer', suffix: 'Seconds', - defaultValue: '60' + defaultValue: '60', + required: true, + min: 1, + max: 600 }, 'bt-tracker-interval': { type: 'integer', suffix: 'Seconds', - defaultValue: '0' + defaultValue: '0', + required: true, + min: 0 }, 'bt-tracker-timeout': { type: 'integer', suffix: 'Seconds', - defaultValue: '60' + defaultValue: '60', + required: true, + min: 1, + max: 600 }, 'dht-file-path': { type: 'string', @@ -326,7 +409,7 @@ defaultValue: '$HOME/.aria2/dht6.dat' }, 'dht-listen-port': { - type: 'integer', + type: 'string', readonly: true, defaultValue: '6881-6999' }, @@ -347,12 +430,14 @@ }, 'enable-peer-exchange': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'follow-torrent': { type: 'option', options: ['true', 'false', 'mem'], - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'listen-port': { type: 'integer', @@ -362,12 +447,16 @@ 'max-overall-upload-limit': { type: 'string', suffix: 'Bytes', - defaultValue: '0' + defaultValue: '0', + required: true, + pattern: '^(0|[1-9]\\d*(K|M)?)$' }, 'max-upload-limit': { type: 'string', suffix: 'Bytes', - defaultValue: '0' + defaultValue: '0', + required: true, + pattern: '^(0|[1-9]\\d*(K|M)?)$' }, 'peer-id-prefix': { type: 'string', @@ -376,16 +465,21 @@ }, 'seed-ratio': { type: 'float', - defaultValue: '1.0' + defaultValue: '1.0', + required: true, + min: 0 }, 'seed-time': { type: 'integer', - suffix: 'Minutes' + suffix: 'Minutes', + required: true, + min: 0 }, 'follow-metalink': { type: 'option', options: ['true', 'false', 'mem'], - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'metalink-base-uri': { type: 'string' @@ -405,11 +499,13 @@ 'metalink-preferred-protocol': { type: 'option', options: ['http', 'https', 'ftp', 'none'], - defaultValue: 'none' + defaultValue: 'none', + required: true }, 'metalink-enable-unique-protocol': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'enable-rpc': { type: 'boolean', @@ -418,7 +514,8 @@ }, 'pause-metadata': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'rpc-allow-origin-all': { type: 'boolean', @@ -443,7 +540,8 @@ }, 'rpc-save-upload-metadata': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'rpc-secure': { type: 'boolean', @@ -451,23 +549,28 @@ }, 'allow-overwrite': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'allow-piece-length-change': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'always-resume': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'async-dns': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'auto-file-renaming': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'auto-save-interval': { type: 'integer', @@ -477,7 +580,8 @@ }, 'conditional-get': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'conf-path': { type: 'string', @@ -514,7 +618,8 @@ 'download-result': { type: 'option', options: ['default', 'full', 'hide'], - defaultValue: 'default' + defaultValue: 'default', + required: true }, 'dscp': { type: 'string', @@ -531,7 +636,8 @@ }, 'enable-mmap': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'event-poll': { type: 'option', @@ -541,15 +647,18 @@ 'file-allocation': { type: 'option', options: ['none', 'prealloc', 'trunc', 'falloc'], - defaultValue: 'prealloc' + defaultValue: 'prealloc', + required: true }, 'force-save': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'hash-check-only': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'human-readable': { type: 'boolean', @@ -558,16 +667,22 @@ }, 'max-download-result': { type: 'integer', - defaultValue: '1000' + defaultValue: '1000', + required: true, + min: 0 }, 'max-mmap-limit': { type: 'string', suffix: 'Bytes', - defaultValue: '9223372036854775807' + defaultValue: '9223372036854775807', + required: true, + pattern: '^(0|[1-9]\\d*(K|M)?)$' }, 'max-resume-failure-tries': { type: 'integer', - defaultValue: '0' + defaultValue: '0', + required: true, + min: 0 }, 'min-tls-version': { type: 'option', @@ -578,15 +693,19 @@ 'log-level': { type: 'option', options: ['debug', 'info', 'notice', 'warn', 'error'], - defaultValue: 'debug' + defaultValue: 'debug', + required: true }, 'optimize-concurrent-downloads': { type: 'string', defaultValue: 'false' }, 'piece-length': { - type: 'integer', - defaultValue: '1M' + type: 'string', + suffix: 'Bytes', + defaultValue: '1M', + required: true, + pattern: '^(0|[1-9]\\d*M?)$' }, 'show-console-readout': { type: 'boolean', @@ -602,12 +721,16 @@ 'max-overall-download-limit': { type: 'string', suffix: 'Bytes', - defaultValue: '0' + defaultValue: '0', + required: true, + pattern: '^(0|[1-9]\\d*(K|M)?)$' }, 'max-download-limit': { type: 'string', suffix: 'Bytes', - defaultValue: '0' + defaultValue: '0', + required: true, + pattern: '^(0|[1-9]\\d*(K|M)?)$' }, 'no-conf': { type: 'boolean', @@ -616,11 +739,14 @@ 'no-file-allocation-limit': { type: 'string', suffix: 'Bytes', - defaultValue: '5M' + defaultValue: '5M', + required: true, + pattern: '^(0|[1-9]\\d*(K|M)?)$' }, 'parameterized-uri': { type: 'boolean', - defaultValue: 'false' + defaultValue: 'false', + required: true }, 'quiet': { type: 'boolean', @@ -629,10 +755,12 @@ }, 'realtime-chunk-checksum': { type: 'boolean', - defaultValue: 'true' + defaultValue: 'true', + required: true }, 'remove-control-file': { - type: 'boolean' + type: 'boolean', + required: true }, 'save-session': { type: 'string' diff --git a/src/scripts/config/constants.js b/src/scripts/config/constants.js index efa5520..2887565 100644 --- a/src/scripts/config/constants.js +++ b/src/scripts/config/constants.js @@ -7,7 +7,8 @@ optionStorageKey: 'Options', globalStatStorageCapacity: 120, taskStatStorageCapacity: 300, - lazySaveTimeout: 500 + lazySaveTimeout: 500, + errorTooltipDelay: 200 }).constant('ariaNgDefaultOptions', { language: 'en', rpcHost: '', diff --git a/src/scripts/config/defaultLanguage.js b/src/scripts/config/defaultLanguage.js index 06f930a..2f93681 100644 --- a/src/scripts/config/defaultLanguage.js +++ b/src/scripts/config/defaultLanguage.js @@ -124,6 +124,11 @@ 'Changes to the settings take effect after refreshing page.': 'Changes to the settings take effect after refreshing page.', 'Type is illegal!': 'Type is illegal!', 'Parameter is invalid!': 'Parameter is invalid!', + 'Option value cannot be empty!': 'Option value cannot be empty!', + 'Input number is invalid!': 'Input number is invalid!', + 'Input number is below min value!': 'Input number is below min value {{value}}!', + 'Input number is above max value!': 'Input number is above max value {{value}}!', + 'Input value is invalid!': 'Input value is invalid!', 'format': { 'longdate': 'MM/DD/YYYY HH:mm:ss', 'time.millisecond': '{{value}} Millisecond', diff --git a/src/scripts/directives/setting.js b/src/scripts/directives/setting.js index 44c7b7d..0f21d6b 100644 --- a/src/scripts/directives/setting.js +++ b/src/scripts/directives/setting.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - angular.module('ariaNg').directive('ngSetting', ['$timeout', 'ariaNgConstants', function ($timeout, ariaNgConstants) { + angular.module('ariaNg').directive('ngSetting', ['$timeout', '$translate', 'ariaNgConstants', function ($timeout, $translate, ariaNgConstants) { return { restrict: 'E', templateUrl: 'views/setting.html', @@ -16,26 +16,26 @@ link: function (scope, element, attrs, ngModel) { var pendingSaveRequest = null; var options = { - lazySaveTimeout: ariaNgConstants.lazySaveTimeout + lazySaveTimeout: ariaNgConstants.lazySaveTimeout, + errorTooltipDelay: ariaNgConstants.errorTooltipDelay }; angular.extend(options, attrs); - scope.optionStatus = (function () { - var value = 'ready'; + var destroyTooltip = function () { + angular.element(element).tooltip('destroy'); + }; - var destroyTooltip = function () { - angular.element(element).tooltip('destroy'); - }; - - var showTooltip = function (cause, type) { - if (!cause) { - return; - } + var showTooltip = function (cause, type, causeParams) { + if (!cause) { + return; + } + $timeout(function () { angular.element(element).tooltip({ - title: cause, + title: $translate.instant(cause, causeParams), trigger: 'focus', + placement: 'auto top', container: element, template: '' }).tooltip('show'); - }; + }, options.errorTooltipDelay); + }; + + scope.optionStatus = (function () { + var value = 'ready'; return { getValue: function () { @@ -70,10 +74,10 @@ value = 'failed'; showTooltip(cause, 'warning'); }, - setError: function (cause) { + setError: function (cause, causeParams) { destroyTooltip(); value = 'error'; - showTooltip(cause, 'error'); + showTooltip(cause, 'error', causeParams); }, getStatusFeedbackStyle: function () { if (value == 'success') { @@ -116,6 +120,10 @@ }; scope.changeValue = function (optionValue, lazySave) { + if (pendingSaveRequest) { + $timeout.cancel(pendingSaveRequest); + } + scope.optionValue = optionValue; scope.optionStatus.setReady(); @@ -123,16 +131,52 @@ return; } + if (scope.option.required && optionValue == '') { + scope.optionStatus.setError('Option value cannot be empty!'); + return; + } + + if (scope.option.type == 'integer' && !/^-?\d+$/.test(optionValue)) { + scope.optionStatus.setError('Input number is invalid!'); + return; + } + + if (scope.option.type == 'float' && !/^-?(\d*\.)?\d+$/.test(optionValue)) { + scope.optionStatus.setError('Input number is invalid!'); + return; + } + + if ((scope.option.type == 'integer' || scope.option.type == 'float') && (!angular.isUndefined(scope.option.min) || !angular.isUndefined(scope.option.max))) { + var number = optionValue; + + if (scope.option.type == 'integer') { + number = parseInt(optionValue); + } else if (scope.option.type == 'float') { + number = parseFloat(optionValue); + } + + if (!angular.isUndefined(scope.option.min) && number < scope.option.min) { + scope.optionStatus.setError('Input number is below min value!', { value: scope.option.min }); + return; + } + + if (!angular.isUndefined(scope.option.max) && number > scope.option.max) { + scope.optionStatus.setError('Input number is above max value!', { value: scope.option.max }); + return; + } + } + + if (!angular.isUndefined(scope.option.pattern) && !(new RegExp(scope.option.pattern).test(optionValue))) { + scope.optionStatus.setError('Input value is invalid!'); + return; + } + var data = { key: scope.option.key, value: optionValue, optionStatus: scope.optionStatus }; - if (pendingSaveRequest) { - $timeout.cancel(pendingSaveRequest); - } - var invokeChange = function () { scope.optionStatus.setSaving(); scope.onChangeValue(data);