diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..a5fe8e7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [erikdubbelboer] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4dc5d11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +config.inc.php +*.phar +vendor + +# IDEs metadata +/nbproject/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 5ad0d4b..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "predis"] - path = predis - url = git://github.com/nrk/predis.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..610dbda --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM composer:2.2 + +RUN apk add --no-cache tini tzdata + +WORKDIR /src/app + +COPY . . + +RUN set -xe; \ + composer install; \ + cp includes/config.environment.inc.php includes/config.inc.php + +ENV PORT 80 +EXPOSE 80 +ENTRYPOINT [ "sh", "-c", "tini -- php -S 0.0.0.0:$PORT" ] diff --git a/README.markdown b/README.markdown index 4241b4d..0938c08 100644 --- a/README.markdown +++ b/README.markdown @@ -1,35 +1,71 @@ phpRedisAdmin ============= -phpRedisAdmin is a simple web interface to manage [Redis](http://redis.io/) databases. It is released under the [Creative Commons Attribution 3.0 license](http://creativecommons.org/licenses/by/3.0/). This code is being developed and maintained by [Erik Dubbelboer](https://github.com/ErikDubbelboer/). +phpRedisAdmin is a simple web interface to manage [Redis](http://redis.io/) +databases. It is released under the +[Creative Commons Attribution 3.0 license](http://creativecommons.org/licenses/by/3.0/). +This code is being developed and maintained by [Erik Dubbelboer](https://github.com/ErikDubbelboer/). -You can send comments, patches, questions [here on github](https://github.com/ErikDubbelboer/phpRedisAdmin/issues) or to erik@dubbelboer.com. +You can send comments, patches, questions +[here on github](https://github.com/ErikDubbelboer/phpRedisAdmin/issues) +or to erik@dubbelboer.com. Example ======= -You can find an example database at [http://dubbelboer.com/phpRedisAdmin/](http://dubbelboer.com/phpRedisAdmin/) +You can find an example database at +[http://dubbelboer.com/phpRedisAdmin/](http://dubbelboer.com/phpRedisAdmin/) Installing/Configuring ====================== -To install phpRedisAdmin in the current directory you need to execute the following commands: +To install [phpRedisAdmin](https://packagist.org/packages/erik-dubbelboer/php-redis-admin) through [composer](http://getcomposer.org/) you need to execute the following commands: + +``` +curl -s http://getcomposer.org/installer | php +php composer.phar create-project -s dev erik-dubbelboer/php-redis-admin path/to/install +``` + +You may also want to copy includes/config.sample.inc.php to includes/config.inc.php +and edit it with your specific redis configuration. + +Instead of using composer, you can also do a manual install using: ``` git clone https://github.com/ErikDubbelboer/phpRedisAdmin.git cd phpRedisAdmin -git submodule init -git submodule update +git clone https://github.com/nrk/predis.git vendor ``` -You will also need to edit config.inc.php with your redis information. +Docker Image +============ +A public [phpRedisAdmin Docker image](https://hub.docker.com/r/erikdubbelboer/phpredisadmin/) is available on Docker Hub built from the latest tag. +The file ```includes/config.environment.inc.php``` is used as the configuration file to allow environment variables to be used as configuration values. +Example: +``` +docker run --rm -it -e REDIS_1_HOST=myredis.host -e REDIS_1_NAME=MyRedis -p 80:80 erikdubbelboer/phpredisadmin +``` +Also, a Docker Compose manifest with a stack for testing and development is provided. Just issue ```docker-compose up --build``` to start it and browse to http://localhost. See ```docker-compose.yml``` file for configuration details. +Environment variables summary +==== + +* ``REDIS_1_HOST`` - define host of the Redis server +* ``REDIS_1_NAME`` - define name of the Redis server +* ``REDIS_1_PORT`` - define port of the Redis server +* ``REDIS_1_SCHEME`` - define scheme of the Redis server (tcp or tls) +* ``REDIS_1_AUTH`` - define password of the Redis server +* ``REDIS_1_AUTH_FILE`` - define file containing the password of the Redis server +* ``REDIS_1_DATABASES`` - You can modify you config to prevent phpRedisAdmin from using CONFIG command +* ``ADMIN_USER`` - define username for user-facing Basic Auth +* ``ADMIN_PASS`` - define password for user-facing Basic Auth TODO ==== +* Encoding support for editing * Javascript sorting of tables * Better error handling * Move or Copy key to different server @@ -43,4 +79,3 @@ Credits Icons by [http://p.yusukekamiyamane.com/](http://p.yusukekamiyamane.com/) ([https://github.com/yusukekamiyamane/fugue-icons/tree/master/icons-shadowless](https://github.com/yusukekamiyamane/fugue-icons/tree/master/icons-shadowless)) Favicon from [https://github.com/antirez/redis-io/blob/master/public/images/favicon.png](https://github.com/antirez/redis-io/blob/master/public/images/favicon.png) - diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1627ff2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 1.13.x | :white_check_mark: | +| 1.12.x | :white_check_mark: | +| < 1.12 | :x: | + +## Reporting a Vulnerability + +Vulnerabilities can be emailed to erik@dubbelboer.com diff --git a/common.inc.php b/common.inc.php deleted file mode 100644 index ac0303a..0000000 --- a/common.inc.php +++ /dev/null @@ -1,90 +0,0 @@ - $v) { - unset($process[$key][$k]); - - if (is_array($v)) { - $process[$key][stripslashes($k)] = $v; - $process[] = &$process[$key][stripslashes($k)]; - } else { - $process[$key][stripslashes($k)] = stripslashes($v); - } - } - } - - unset($process); -} - - - - -// These includes are needed by each script. -require_once 'config.inc.php'; -require_once 'functions.inc.php'; -require_once 'page.inc.php'; -require_once 'predis/autoload.php'; - - -if (isset($config['login'])) { - require_once 'login.inc.php'; -} - - - - -if (isset($login['servers'])) { - $i = current($login['servers']); -} else { - $i = 0; -} - - -if (isset($_GET['s']) && is_numeric($_GET['s']) && ($_GET['s'] < count($config['servers']))) { - $i = $_GET['s']; -} - -$server = $config['servers'][$i]; -$server['id'] = $i; - - -if (isset($login, $login['servers'])) { - if (array_search($i, $login['servers']) === false) { - die('You are not allowed to access this database.'); - } - - foreach ($config['servers'] as $key => $ignore) { - if (array_search($key, $login['servers']) === false) { - unset($config['servers'][$key]); - } - } -} - - -if (!isset($server['db'])) { - $server['db'] = 0; -} - - -// Setup a connection to Redis. -$redis = new Predis\Client('tcp://'.$server['host'].':'.$server['port']); - -if (isset($server['auth'])) { - if (!$redis->auth($server['auth'])) { - die('ERROR: Authentication failed ('.$server['host'].':'.$server['port'].')'); - } -} - - -if ($server['db'] != 0) { - if (!$redis->select($server['db'])) { - die('ERROR: Selecting database failed ('.$server['host'].':'.$server['port'].','.$server['db'].')'); - } -} - -?> diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3b3ab81 --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "erik-dubbelboer/php-redis-admin", + "description": "Simple web interface to manage Redis databases.", + "version": "1.24.0", + "license": "CC-BY-3.0", + "homepage": "https://github.com/ErikDubbelboer/phpRedisAdmin", + "authors": [ + { + "name": "Erik Dubbelboer", + "email": "erik@dubbelboer.com", + "homepage": "http://blog.dubbelboer.com/", + "role": "Developer" + } + ], + "require": { + "ext-mbstring": "*", + "ext-json": "*", + "predis/predis": "v2.3.0", + "paragonie/random_compat": ">=2" + }, + "minimum-stability": "stable", + "target-dir": "ErikDubbelboer/phpRedisAdmin" +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..f724f31 --- /dev/null +++ b/composer.lock @@ -0,0 +1,133 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "401ff61cebe5223d003a47192d7c3d6a", + "packages": [ + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com/" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "predis/predis", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/predis/predis.git", + "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/predis/predis/zipball/bac46bfdb78cd6e9c7926c697012aae740cb9ec9", + "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ^9.4" + }, + "suggest": { + "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Till Krüss", + "homepage": "https://till.im/", + "role": "Maintainer" + } + ], + "description": "A flexible and feature-complete Redis client for PHP.", + "homepage": "http://github.com/predis/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "support": { + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2024-11-21T20:00:02+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "ext-mbstring": "*", + "ext-json": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/config.inc.php b/config.inc.php deleted file mode 100644 index a0fdf4d..0000000 --- a/config.inc.php +++ /dev/null @@ -1,56 +0,0 @@ - array( - 0 => array( - 'name' => 'local server', // Optional name. - 'host' => '127.0.0.1', - 'port' => 6379, - - // Optional Redis authentication. - //'auth' => 'redispasswordhere' // Warning: The password is sent in plain-text to the Redis server. - ), - - /*1 => array( - 'host' => 'localhost', - 'port' => 6380 - ),*/ - - /*2 => array( - 'name' => 'local db 2', - 'host' => 'localhost', - 'port' => 6379, - 'db' => 1 // Optional database number, see http://redis.io/commands/select - )*/ - ), - - - 'seperator' => ':', - - - // Uncomment to show less information and make phpRedisAdmin fire less commands to the Redis server. Recommended for a really busy Redis server. - //'faster' => true, - - - // Uncomment to enable HTTP authentication - /*'login' => array( - // Username => Password - // Multiple combinations can be used - 'admin' => array( - 'password' => 'adminpassword', - ), - 'guest' => array( - 'password' => '', - 'servers' => array(1) // Optional list of servers this user can access. - ) - ),*/ - - - - - // You can ignore settings below this point. - - 'maxkeylen' => 100 -); - -?> diff --git a/css/common.css b/css/common.css index 942c49a..9dc6276 100644 --- a/css/common.css +++ b/css/common.css @@ -1,4 +1,3 @@ - html { font-size: x-small; /* Wikipedia font-size scaling method */ } @@ -9,8 +8,8 @@ color: #000; margin: 0; padding: 0; border: 0; -height: 100%; -max-height: 100%; +height: 100%; +max-height: 100%; background-color: #fff; } @@ -51,3 +50,8 @@ padding: 3px 0 1px 20px; background: url(http://github.com/images/add.png) left center no-repeat; } + +.data { +white-space: pre-wrap; +} + diff --git a/css/frame.css b/css/frame.css index c467b85..7cf935d 100644 --- a/css/frame.css +++ b/css/frame.css @@ -1,4 +1,3 @@ - form { margin: 0; } @@ -15,7 +14,6 @@ margin-left: -8em; } form .button { -margin-left: -7em; } @@ -54,3 +52,13 @@ float: left; margin: 1em; } + +.exception { +border: 1px solid #ff0000; +background-color: rgba(255, 0, 0, 0.2); +color: #880000; +padding: 10px; +border-radius: 5px; +margin: 20px; +} + diff --git a/css/index.css b/css/index.css index b562ccf..4a56aa9 100644 --- a/css/index.css +++ b/css/index.css @@ -1,14 +1,16 @@ - #sidebar { position: absolute; top: 0; -bottom: 0; +bottom: 0; left: 0; -width: 24em; +display: flex; +flex-direction: column; +width: 290px; height: 100%; -padding-left: 1em; -border-right: 1px solid #000; -overflow: hidden; +} + +#header { +padding-left: 10px; } #sidebar a, #sidebar a:visited { @@ -25,11 +27,10 @@ text-decoration: underline; #keys { -position: fixed; -top: 15.5em; -bottom: 0; -width: 24em; -padding-bottom: 1em; +flex-grow: 1; +width: 290px; +margin-top: 10px; +padding-left: 10px; overflow: auto; } @@ -41,11 +42,17 @@ padding: 0; #keys li { font-weight: normal; +white-space: nowrap; } #keys li.folder { font-weight: bold; margin-top: .05em; +cursor: pointer; +} + +#keys li.empty a { +color: #888; } #keys li.current a { @@ -105,18 +112,41 @@ display: none; display: inline; } +#resize { +position: fixed; +top: 0; +left: 300px; +bottom: 0; +width: 5px; +background-color: #aaa; +cursor: col-resize; +padding: 0; +margin: 0; +border-left: 1px solid #000; +} + +#resize-layover { +position: fixed; +top: 0; +left: 305px; +right: 0; +bottom: 0; +width: 100%; +z-index: 1000; +} #frame { position: fixed; -top: 0; -left: 25em; +top: 0; +left: 305px; right: 0; bottom: 0; -padding-left: 1em; } #frame iframe { -width: 100%; +width: calc(100% - 3em); height: 100%; +margin-left: 1.5em; +margin-right: 1.5em; } diff --git a/css/login.css b/css/login.css new file mode 100644 index 0000000..f439934 --- /dev/null +++ b/css/login.css @@ -0,0 +1,116 @@ +/* Styles borrowed from http://getbootstrap.com/examples/signin/ */ +h1.logo { + text-align: center; +} +h2 { + font-size: 30px; +} +h2 { + margin-top: 20px; + margin-bottom: 10px; +} +h2 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +.form-signin { + max-width: 330px; + padding: 15px; + margin: 0 auto; +} +.form-signin .form-signin-heading, +.form-signin .form-control { + position: relative; + height: auto; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 10px; +} +.form-signin .form-control:focus { + z-index: 2; +} +.form-signin input[type="text"] { + margin-bottom: -1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.form-signin input[type="password"] { + margin-bottom: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; +} +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-lg { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; +} +.invalid-credentials { + border: 1px solid #ff0000; + background-color: rgba(255, 0, 0, 0.2); + color: #880000; + padding: 10px; + border-radius: 5px; + margin: 20px; +} diff --git a/delete.php b/delete.php index f37f306..c329919 100644 --- a/delete.php +++ b/delete.php @@ -1,13 +1,13 @@ del($key); } - - die('?&s='.$server['id']); + + die('?view&s='.$server['id'].'&d='.$server['db']); +} + +if (isset($_GET['batch_del'])) { + if (empty($_POST['selected_keys'])) { + die('No keys to delete'); + } + $keys = json_decode($_POST['selected_keys']); + + foreach ($keys as $key) { + $redis->del($key); + } + + die('?view&s=' . $server['id'] . '&d=' . $server['db'] . '&key=' . urlencode($keys[0])); } -?> diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5ef83df --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + phpredisadmin: + build: . + environment: + - ADMIN_USER=admin + - ADMIN_PASS=admin + - REDIS_1_HOST=redis + - REDIS_1_PORT=6379 + links: + - redis + ports: + - "80:80" + + redis: + image: redis + command: --loglevel verbose diff --git a/edit.php b/edit.php index ba9debf..998012c 100644 --- a/edit.php +++ b/edit.php @@ -1,9 +1,8 @@ set($_POST['key'], $_POST['value']); + $key = input_convert($_POST['key']); + $value = input_convert($_POST['value']); + $value = encodeOrDecode('save', $key, $value); + + if ($value === false || is_null($value)) { + die('ERROR: could not encode value'); } - // Hash - else if (($_POST['type'] == 'hash') && isset($_POST['hkey'])) { - if (strlen($_POST['hkey']) > $config['maxkeylen']) { - die('ERROR: Your hash key is to long (max length is '.$config['maxkeylen'].')'); + try { + // String + if ($_POST['type'] == 'string') { + $redis->set($key, $value); } - if ($edit && !$redis->hExists($_POST['key'], $_POST['hkey'])) { - $redis->hDel($_POST['key'], $_GET['hkey']); - } + // Hash + else if (($_POST['type'] == 'hash') && isset($_POST['hkey'])) { + if (strlen($_POST['hkey']) > $config['maxkeylen']) { + die('ERROR: Your hash key is to long (max length is '.$config['maxkeylen'].')'); + } - $redis->hSet($_POST['key'], $_POST['hkey'], $_POST['value']); - } + if ($edit && !$redis->hExists($key, input_convert($_POST['hkey']))) { + $redis->hDel($key, input_convert($_GET['hkey'])); + } - // List - else if (($_POST['type'] == 'list') && isset($_POST['index'])) { - $size = $redis->lLen($_POST['key']); - - if (($_POST['index'] == '') || - ($_POST['index'] == $size) || - ($_POST['index'] == -1)) { - // Push it at the end - $redis->rPush($_POST['key'], $_POST['value']); - } else if (($_POST['index'] >= 0) && - ($_POST['index'] < $size)) { - // Overwrite an index - $redis->lSet($_POST['key'], $_POST['index'], $_POST['value']); - } else { - die('ERROR: Out of bounds index'); + $redis->hSet($key, input_convert($_POST['hkey']), $value); } - } - // Set - else if ($_POST['type'] == 'set') { - if ($_POST['value'] != $_POST['oldvalue']) { - // The only way to edit a Set value is to add it and remove the old value. - $redis->sRem($_POST['key'], $_POST['oldvalue']); - $redis->sAdd($_POST['key'], $_POST['value']); + // List + else if (($_POST['type'] == 'list') && isset($_POST['index'])) { + $size = $redis->lLen($key); + + if (($_POST['index'] == '') || + ($_POST['index'] == $size)) { + // Push it at the end + $redis->rPush($key, $value); + } else if ($_POST['index'] == -1) { + // Push it at the start + $redis->lPush($key, $value); + } else if (($_POST['index'] >= 0) && + ($_POST['index'] < $size)) { + // Overwrite an index + $redis->lSet($key, input_convert($_POST['index']), $value); + } else { + die('ERROR: Out of bounds index'); + } } - } - // ZSet - else if (($_POST['type'] == 'zset') && isset($_POST['score'])) { - if ($_POST['value'] != $_POST['oldvalue']) { + // Set + else if ($_POST['type'] == 'set') { + if ($_POST['value'] != $_POST['oldvalue']) { + // The only way to edit a Set value is to add it and remove the old value. + $redis->sRem($key, encodeOrDecode('save', $key, input_convert($_POST['oldvalue']))); + $redis->sAdd($key, $value); + } + } + + // ZSet + else if (($_POST['type'] == 'zset') && isset($_POST['score']) && is_numeric($_POST['score'])) { // The only way to edit a ZSet value is to add it and remove the old value. - $redis->zRem($_POST['key'], $_POST['oldvalue']); - $redis->zAdd($_POST['key'], $_POST['score'], $_POST['value']); + $redis->zRem($key, encodeOrDecode('save', $key, input_convert($_POST['oldvalue']))); + $redis->zAdd($key, input_convert($_POST['score']), $value); } - } - // Refresh the top so the key tree is updated. - require 'header.inc.php'; + // Refresh the top so the key tree is updated. + require 'includes/header.inc.php'; - ?> - - + + +
+

getMessage() ?>

+
+

-
+ +

@@ -149,12 +170,12 @@

-> +>

-> +>

@@ -169,18 +190,16 @@

- +

-

-

+?> \ No newline at end of file diff --git a/export.php b/export.php index 4757732..d17839e 100644 --- a/export.php +++ b/export.php @@ -1,20 +1,25 @@ type($key); + // we rename the keys as necessary + if($filter !== false && $transform !== false) + $outputKey = str_replace($filter, $transform, $key); + else + $outputKey = $key; // String if ($type == 'string') { - echo 'SET "',addslashes($key),'" "',addslashes($redis->get($key)),'"',PHP_EOL; + echo 'SET "',addslashes($outputKey),'" "',addslashes($redis->get($key)),'"',PHP_EOL; } // Hash @@ -22,7 +27,7 @@ function export_redis($key) { $values = $redis->hGetAll($key); foreach ($values as $k => $v) { - echo 'HSET "',addslashes($key),'" "',addslashes($k),'" "',addslashes($v),'"',PHP_EOL; + echo 'HSET "',addslashes($outputKey),'" "',addslashes($k),'" "',addslashes($v),'"',PHP_EOL; } } @@ -31,7 +36,7 @@ function export_redis($key) { $size = $redis->lLen($key); for ($i = 0; $i < $size; ++$i) { - echo 'RPUSH "',addslashes($key),'" "',addslashes($redis->lIndex($key, $i)),'"',PHP_EOL; + echo 'RPUSH "',addslashes($outputKey),'" "',addslashes($redis->lIndex($key, $i)),'"',PHP_EOL; } } @@ -40,7 +45,7 @@ function export_redis($key) { $values = $redis->sMembers($key); foreach ($values as $v) { - echo 'SADD "',addslashes($key),'" "',addslashes($v),'"',PHP_EOL; + echo 'SADD "',addslashes($outputKey),'" "',addslashes($v),'"',PHP_EOL; } } @@ -51,7 +56,7 @@ function export_redis($key) { foreach ($values as $v) { $s = $redis->zScore($key, $v); - echo 'ZADD "',addslashes($key),'" ',$s,' "',addslashes($v),'"',PHP_EOL; + echo 'ZADD "',addslashes($outputKey),'" ',$s,' "',addslashes($v),'"',PHP_EOL; } } } @@ -64,7 +69,6 @@ function export_json($key) { $type = $redis->type($key); - // String if ($type == 'string') { $value = $redis->get($key); @@ -115,10 +119,13 @@ function export_json($key) { header('Content-type: '.$ct.'; charset=utf-8'); header('Content-Disposition: inline; filename="export.'.$ext.'"'); - - - // JSON + + $filter = !empty($_POST['filter']) ? trim($_POST['filter']) : false; + $transform = !empty($_POST['transform']) ? trim($_POST['transform']) : false; + + // JSON if ($_POST['type'] == 'json') { + // Single key if (isset($_GET['key'])) { echo json_encode(export_json($_GET['key'])); @@ -127,7 +134,18 @@ function export_json($key) { $vals = array(); foreach ($keys as $key) { - $vals[$key] = export_json($key); + + // if we have a filter and no match, nothing to do + if($filter !== false && stripos($key, $filter) === false) + continue; + + // we rename the keys as necessary + if($filter !== false && $transform !== false) + $outputKey = str_replace($filter, $transform, $key); + else + $outputKey = $key; + + $vals[$outputKey] = export_json($key); } echo json_encode($vals); @@ -136,6 +154,7 @@ function export_json($key) { // Redis Commands else { + // Single key if (isset($_GET['key'])) { export_redis($_GET['key']); @@ -143,7 +162,12 @@ function export_json($key) { $keys = $redis->keys('*'); foreach ($keys as $key) { - export_redis($key); + + // if we have a filter and no match, we skip + if($filter !== false && stripos($key, $filter) === false) + continue; + + export_redis($key, $filter, $transform); } } } @@ -158,12 +182,13 @@ function export_json($key) { $page['css'][] = 'frame'; $page['js'][] = 'frame'; -require 'header.inc.php'; +require 'includes/header.inc.php'; ?>

Export

-
+ +

@@ -173,13 +198,23 @@ function export_json($key) {

-

+ +

+ + +

+ +

+ + +

+ + -

diff --git a/flush.php b/flush.php new file mode 100644 index 0000000..c8fc1f5 --- /dev/null +++ b/flush.php @@ -0,0 +1,11 @@ +flushdb(); + diff --git a/functions.inc.php b/functions.inc.php deleted file mode 100644 index 388f535..0000000 --- a/functions.inc.php +++ /dev/null @@ -1,66 +0,0 @@ -= 0) - $suffix = 'ago'; - else { - $when = -$when; - $suffix = 'in the future'; - } - - if ($when > $day) { - $when = round($when / $day); - $what = 'day'; - } else if ($when > $hour) { - $when = round($when / $hour); - $what = 'hour'; - } else if ($when > $minute) { - $when = round($when / $minute); - $what = 'minute'; - } else { - $what = 'second'; - } - - if ($when != 1) $what .= 's'; - - if ($ago) { - return "$when $what $suffix"; - } else { - return "$when $what"; - } -} - - -function format_size($size) { - $sizes = array('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'); - - if ($size == 0) { - return '0 B'; - } else { - return round($size / pow(1024, ($i = floor(log($size, 1024)))), 1).' '.$sizes[$i]; - } -} - - -function str_rand($length) { - $r = ''; - - for (; $length > 0; --$length) { - $r .= chr(rand(32, 126)); // 32 - 126 is the printable ascii range - } - - return $r; -} - diff --git a/images/flush.png b/images/flush.png new file mode 100644 index 0000000..54c7d16 Binary files /dev/null and b/images/flush.png differ diff --git a/import.php b/import.php index dc8bebc..5e5a89e 100644 --- a/import.php +++ b/import.php @@ -1,9 +1,7 @@ lPush($commands[$i+1], $commands[$i+2]); $i += 2; @@ -69,15 +67,15 @@ // Refresh the top so the key tree is updated. - require 'header.inc.php'; + require 'includes/header.inc.php'; ?>

Import

-
+ +

-

-

diff --git a/includes/common.inc.php b/includes/common.inc.php new file mode 100644 index 0000000..78e791f --- /dev/null +++ b/includes/common.inc.php @@ -0,0 +1,159 @@ + $ignore) { + if (array_search($key, $login['servers']) === false) { + unset($config['servers'][$key]); + } + } +} + + +if (!isset($server['db'])) { + if (isset($_GET['d']) && is_numeric($_GET['d'])) { + $server['db'] = $_GET['d']; + } else { + $server['db'] = 0; + } +} + + +if (!isset($server['filter'])) { + $server['filter'] = '*'; +} + +// filter from GET param +if (isset($_GET['filter']) && $_GET['filter'] != '') { + $server['filter'] = $_GET['filter']; + if (strpos($server['filter'], '*') === false) { + $server['filter'].= '*'; + } +} + +if (!isset($server['seperator'])) { + $server['seperator'] = $config['seperator']; +} + +if (!isset($server['keys'])) { + $server['keys'] = $config['keys']; +} + +if (!isset($server['scansize'])) { + if (isset($config['scansize'])) { + $server['scansize'] = $config['scansize']; + } else { + $server['scansize'] = 1000; + } +} + +if (!isset($server['scanmax'])) { + if (isset($config['scanmax'])) { + $server['scanmax'] = $config['scanmax']; + } else { + $server['scanmax'] = 0; + } +} + +if (!isset($server['serialization'])) { + if (isset($config['serialization'])) { + $server['serialization'] = $config['serialization']; + } +} + +if (!isset($config['hideEmptyDBs'])) { + $config['hideEmptyDBs'] = false; +} + +if (!isset($config['showEmptyNamespaceAsKey'])) { + $config['showEmptyNamespaceAsKey'] = false; +} + +if (!isset($server['scheme']) || empty($server['scheme'])) { + $server['scheme'] = 'tcp'; +} + +// Setup a connection to Redis. +if ($server['scheme'] === 'unix' && $server['path']) { + $redis = new Predis\Client(array('scheme' => 'unix', 'path' => $server['path'])); +} else { + $redis = !$server['port'] ? new Predis\Client($server['host']) : new Predis\Client($server['scheme'].'://'.$server['host'].':'.$server['port']); +} + +try { + $redis->connect(); +} catch (Predis\CommunicationException $exception) { + die('ERROR: ' . $exception->getMessage()); +} + +if (isset($server['auth'])) { + if (!$redis->auth($server['auth'])) { + die('ERROR: Authentication failed ('.$server['host'].':'.$server['port'].')'); + } +} + +if (!isset($config['login']) && !empty($config['login_as_acl_auth'])) { + require_once PHPREDIS_ADMIN_PATH . '/includes/login_acl.inc.php'; +} + +if ($server['db'] != 0) { + if (!$redis->select($server['db'])) { + die('ERROR: Selecting database failed ('.$server['host'].':'.$server['port'].','.$server['db'].')'); + } +} diff --git a/includes/config.environment.inc.php b/includes/config.environment.inc.php new file mode 100644 index 0000000..06a06fe --- /dev/null +++ b/includes/config.environment.inc.php @@ -0,0 +1,86 @@ + array( + 'password' => $admin_pass, + ), + ); +} + +$i=1; +$config['servers'] = array(); + +while (true) { + + $prefix = 'REDIS_' . $i . '_'; + + $server_name = getenv($prefix . 'NAME'); + $server_host = getenv($prefix . 'HOST'); + $server_port = getenv($prefix . 'PORT'); + $server_scheme = getenv($prefix . 'SCHEME'); + if (getenv($prefix . 'AUTH_FILE') !== false) { + $server_auth = file_get_contents(getenv($prefix . 'AUTH_FILE')); + } else { + $server_auth = getenv($prefix . 'AUTH'); + } + $server_databases = getenv($prefix . 'DATABASES'); + + if (empty($server_host)) { + break; + } + + if (empty($server_name)) { + $server_name = $server_host; + } + + if (empty($server_auth)) { + $server_auth = ""; + } + + if (empty($server_port) && strpos($server_host, ':') === false) { + $server_port = 6379; + } + + if (empty($server_scheme)) { + $server_scheme = 'tcp'; + } + + $config['servers'][] = array( + 'name' => $server_name, + 'host' => $server_host, + 'port' => $server_port, + 'filter' => $config['filter'], + 'scansize' => $config['scansize'], + 'scanmax' => $config['scanmax'], + 'scheme' => $server_scheme, + ); + + if (!empty($server_auth)) { + $config['servers'][$i-1]['auth'] = $server_auth; + } + + if (!empty($server_databases)) { + $config['servers'][$i-1]['databases'] = $server_databases; + } + + $i++; +} diff --git a/includes/config.sample.inc.php b/includes/config.sample.inc.php new file mode 100644 index 0000000..923a60c --- /dev/null +++ b/includes/config.sample.inc.php @@ -0,0 +1,97 @@ + array( + array( + 'name' => 'local server', // Optional name. + 'host' => '127.0.0.1', + 'port' => 6379, + 'filter' => '*', + 'scheme' => 'tcp', // Optional. Connection scheme. 'tcp' - for TCP connection, 'unix' - for connection by unix domain socket + 'path' => '', // Optional. Path to unix domain socket. Uses only if 'scheme' => 'unix'. Example: '/var/run/redis/redis.sock' + 'hide' => false, // Optional. Override global setting. Hide empty databases in the database list. + + // Optional Redis authentication. + //'auth' => 'redispasswordhere' // Warning: The password is sent in plain-text to the Redis server. + ), + + /*array( + 'host' => 'localhost', + 'port' => 6380 + ),*/ + + /*array( + 'name' => 'local db 2', + 'host' => 'localhost', + 'port' => 6379, + 'db' => 1, // Optional database number, see http://redis.io/commands/select + 'databases' => 1, // Optional number of databases (prevents use of CONFIG command). + 'filter' => 'something:*', // Show only parts of database for speed or security reasons. + 'seperator' => '/', // Use a different seperator on this database (default uses config default). + 'flush' => false, // Set to true to enable the flushdb button for this instance. + 'charset' => 'cp1251', // Keys and values are stored in redis using this encoding (default utf-8). + 'keys' => false, // Use the old KEYS command instead of SCAN to fetch all keys for this server (default uses config default). + 'scansize' => 1000, // How many entries to fetch using each SCAN command for this server (default uses config default). + 'scanmax' => 1000, // In each query, SCAN command may be executed several times. To shorten the duration, it is recommended to limit the total number of entries to fetch (default uses config default). + ),*/ + ), + + + 'seperator' => ':', + 'showEmptyNamespaceAsKey' => false, + + // Hide empty databases in the database list (global, valid for all servers unless set at server level) + 'hideEmptyDBs' => false, + + // Uncomment to show less information and make phpRedisAdmin fire less commands to the Redis server. Recommended for a really busy Redis server. + //'faster' => true, + + + // Uncomment to enable HTTP authentication + /*'login' => array( + // Username => Password + // Multiple combinations can be used + 'admin' => array( + 'password' => 'adminpassword', + ), + 'guest' => array( + 'password' => '', + 'servers' => array(1) // Optional list of servers this user can access. + ) + ),*/ + + // Uncomment to enable login as ACL authentication (won't work if 'login' or 'auth' is also used) + // Only support using one server at this moment. + // If you set the default user off, browsers will be redirected to login page. + // The user and password will be stored in browser as plaintext so using HTTPS is strongly recommended. + // 'login_as_acl_auth' => true, + + // Use HTML form/cookie-based auth instead of HTTP Basic/Digest auth + 'cookie_auth' => false, + + + /*'serialization' => array( + 'foo*' => array( // Match like KEYS + // Function called when saving to redis. + 'save' => function($data) { return json_encode(json_decode($data)); }, + // Function called when loading from redis. + 'load' => function($data) { return json_encode(json_decode($data), JSON_PRETTY_PRINT); }, + ), + ),*/ + + + // You can ignore settings below this point. + + 'maxkeylen' => 100, + 'count_elements_page' => 100, + + // Use the old KEYS command instead of SCAN to fetch all keys. + 'keys' => false, + + // How many entries to fetch using each SCAN command. + 'scansize' => 1000, + + // The total number of entries to fetch. Set to 0 or -1 for no limit. + 'scanmax' => 0 +); diff --git a/footer.inc.php b/includes/footer.inc.php similarity index 94% rename from footer.inc.php rename to includes/footer.inc.php index 11a09ca..308b1d0 100644 --- a/footer.inc.php +++ b/includes/footer.inc.php @@ -1,3 +1,2 @@ - diff --git a/includes/functions.inc.php b/includes/functions.inc.php new file mode 100644 index 0000000..589d324 --- /dev/null +++ b/includes/functions.inc.php @@ -0,0 +1,106 @@ + $day) { + $tmpday = floor($when / $day); + $tmphour = floor(($when / $hour) - (24*$tmpday)); + $tmpminute = floor(($when / $minute) - (24*60*$tmpday) - ($tmphour * 60)); + $tmpsec = floor($when - (24*60*60*$tmpday) - ($tmphour * 60 * 60) - ($tmpminute * 60)); + return sprintf("%d day%s %d hour%s %d min%s %d sec%s",$tmpday,($tmpday != 1) ? 's' : '',$tmphour,($tmphour != 1) ? 's' : '',$tmpminute,($tmpminute != 1) ? 's' : '',$tmpsec,($tmpsec != 1) ? 's' : ''); + } else if ($when > $hour) { + $tmphour = floor($when / $hour); + $tmpminute = floor(($when / $minute) - ($tmphour * 60)); + $tmpsec = floor($when - ($tmphour * 60 * 60) - ($tmpminute * 60)); + return sprintf("%d hour%s %d min%s %d sec%s",$tmphour,($tmphour != 1) ? 's' : '',$tmpminute,($tmpminute != 1) ? 's' : '',$tmpsec,($tmpsec != 1) ? 's' : ''); + } else if ($when > $minute) { + $tmpminute = floor($when / $minute); + $tmpsec = floor($when - ($tmpminute * 60)); + return sprintf("%d min%s %d sec%s",$tmpminute,($tmpminute != 1) ? 's' : '',$tmpsec,($tmpsec != 1) ? 's' : ''); + } else { + return sprintf("%d sec%s",$when,($when != 1) ? 's' : ''); + } +} + + +function format_size($size) { + $sizes = array('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'); + + if ($size == 0) { + return '0 B'; + } else { + return round($size / pow(1024, ($i = floor(log($size, 1024)))), 1).' '.$sizes[$i]; + } +} + + +function format_ttl($seconds) { + if ($seconds > 60) { + return sprintf('%d (%s)', $seconds, format_time($seconds)); + } else { + return $seconds; + } +} + + +function str_rand($length) { + $r = ''; + + for (; $length > 0; --$length) { + $r .= chr(rand(32, 126)); // 32 - 126 is the printable ascii range + } + + return $r; +} + + +function encodeOrDecode($action, $key, $data) { + global $server; + + if (isset($_GET['raw']) || !isset($server['serialization'])) { + return $data; + } + + foreach ($server['serialization'] as $pattern => $closures) { + if (fnmatch($pattern, $key)) { + return $closures[$action]($data); + } + } + + return $data; +} + +function getRelativePath($base) { + return substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], $base)); +} diff --git a/header.inc.php b/includes/header.inc.php similarity index 76% rename from header.inc.php rename to includes/header.inc.php index 1d09a88..7679bd6 100644 --- a/header.inc.php +++ b/includes/header.inc.php @@ -1,7 +1,10 @@ @@ -24,16 +27,18 @@ <?php echo format_html($server['host'])?> - phpRedisAdmin - + - - - + + + diff --git a/includes/login.inc.php b/includes/login.inc.php new file mode 100644 index 0000000..8cfb26d --- /dev/null +++ b/includes/login.inc.php @@ -0,0 +1,144 @@ + 1, + 'nc' => 1, + 'cnonce' => 1, + 'qop' => 1, + 'username' => 1, + 'uri' => 1, + 'response' => 1 + ); + + $data = array(); + $keys = implode('|', array_keys($needed_parts)); + + preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER); + + foreach ($matches as $m) { + $data[$m[1]] = $m[3] ? $m[3] : $m[4]; + unset($needed_parts[$m[1]]); + } + + if (!empty($needed_parts)) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); + die; + } + + if (!isset($config['login'][$data['username']])) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); + die('Invalid username and/or password combination.'); + } + + $login = $config['login'][$data['username']]; + $login['name'] = $data['username']; + + $password = md5($login['name'].':'.$realm.':'.$login['password']); + + $response = md5($password.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.md5($_SERVER['REQUEST_METHOD'].':'.$data['uri'])); + + if ($data['response'] !== $response) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); + die('Invalid username and/or password combination.'); + } + + return $login; +} + +// Perform auth using a standard HTML
submission and cookies to save login state +function authCookie() +{ + global $config; + + $generateCookieHash = function($username) use ($config) { + if (!isset($config['login'][$username])) { + throw new \RuntimeException("Invalid username"); + } + + // Storing this value client-side so we need to be careful that it + // doesn't reveal anything nor can be guessed. + // Using SHA512 because MD5, SHA1 are both now considered broken + return hash( + 'sha512', + implode(':', array( + $username, + $_SERVER['HTTP_USER_AGENT'], + $_SERVER['REMOTE_ADDR'], + $config['login'][$username]['password'], + )) + ); + }; + + if (!empty($_COOKIE['phpRedisAdminLogin'])) { + // We have a cookie; is it correct? + // Cookie value looks like "username:password-hash" + $cookieVal = explode(':', $_COOKIE['phpRedisAdminLogin']); + if (count($cookieVal) === 2) { + list($username, $cookieHash) = $cookieVal; + if (isset($config['login'][$username])) { + $userData = $config['login'][$username]; + $expectedHash = $generateCookieHash($username); + + if ($cookieHash === $expectedHash) { + // Correct username & password + return $userData; + } + } + } + } + + if (isset($_POST['username'], $_POST['password'])) { + // Login form submitted; correctly? + if (isset($config['login'][$_POST['username']])) { + $userData = $config['login'][$_POST['username']]; + if ($_POST['password'] === $userData['password']) { + // Correct username & password. Set cookie and redirect to home page + $cookieValue = $_POST['username'] . ':' . $generateCookieHash($_POST['username']); + setcookie('phpRedisAdminLogin', $cookieValue); + + // This should be an absolute URL, but that's a bit of a pain to generate; this will work + header("Location: index.php"); + die(); + } + } + } + + // If we're here, we don't have a valid login cookie and we don't have a + // valid form submission, so redirect to the login page if we aren't + // already on that page + if (!defined('LOGIN_PAGE')) { + header("Location: login.php"); + die(); + } + + // We must be on the login page without a valid cookie or submission + return null; +} + +if (!empty($config['cookie_auth'])) { + $login = authCookie(); +} else { + $login = authHttpDigest(); +} + +?> diff --git a/includes/login_acl.inc.php b/includes/login_acl.inc.php new file mode 100644 index 0000000..e8b7d72 --- /dev/null +++ b/includes/login_acl.inc.php @@ -0,0 +1,100 @@ +auth($login['username'], $login['password']); + } catch (Predis\Response\ServerException $exception) { + return false; + } + return true; +} + +// This fill will perform HTTP basic authentication. The authentication data will be sent and stored as plaintext +// Please make sure to use HTTPS proxy such as Apache or Nginx to prevent traffic eavesdropping. +function authHttpBasic() +{ + $realm = 'phpRedisAdmin'; + + if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) { + try { + global $redis; + $redis->ping(); + } catch (Predis\Response\ServerException $exception) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Basic realm="' . $realm . '"'); + die('NOAUTH -- Authentication required'); + } + } + + $login = [ + 'username' => $_SERVER['PHP_AUTH_USER'], + 'password' => $_SERVER['PHP_AUTH_PW'], + ]; + + if (!authCheck($login)) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Basic realm="' . $realm . '"'); + die('NOAUTH -- Authentication required'); + } + + return $login; +} + +// Perform auth using a standard HTML submission and cookies to save login state +function authCookie() +{ + if (!empty($_COOKIE['phpRedisAdminLogin'])) { + // We have a cookie; is it correct? + // Cookie value looks like "username:password-hash" + $login = explode(':', $_COOKIE['phpRedisAdminLogin'], 2); + if (count($login) === 2) { + $login = [ + 'username' => $login[0], + 'password' => $login[1], + ]; + if (authCheck($login)) { + return $login; + } + } + } + + if (isset($_POST['username'], $_POST['password'])) { + // Login form submitted; correctly? + $login = [ + 'username' => $_POST['username'], + 'password' => $_POST['password'], + ]; + + if (authCheck($login)) { + setcookie('phpRedisAdminLogin', $login['username'] . ':' . $login['password']); + // This should be an absolute URL, but that's a bit of a pain to generate; this will work + header("Location: index.php"); + die(); + } + } + + try { + global $redis; + $redis->ping(); + } catch (Predis\Response\ServerException $exception) { + // If we're here, we don't have a valid login cookie and we don't have a + // valid form submission, so redirect to the login page if we aren't + // already on that page + if (!defined('LOGIN_PAGE')) { + header("Location: login.php"); + die(); + } + } + + // We must be on the login page without a valid cookie or submission + return null; +} + +if (!empty($config['cookie_auth'])) { + $login = authCookie(); +} else { + $login = authHttpBasic(); +} diff --git a/page.inc.php b/includes/page.inc.php similarity index 91% rename from page.inc.php rename to includes/page.inc.php index 474a8c8..2941e68 100644 --- a/page.inc.php +++ b/includes/page.inc.php @@ -1,6 +1,5 @@ array('common'), - 'js' => array() + 'js' => array('jquery') ); ?> \ No newline at end of file diff --git a/index.php b/index.php index 12a1cf2..ea509c7 100644 --- a/index.php +++ b/index.php @@ -1,46 +1,175 @@ keys($server['filter']); + } else { + $next = 0; + $keys = array(); + while (true) { + $r = $redis->scan($next, 'MATCH', $server['filter'], 'COUNT', $server['scansize']); + $next = $r[0]; + $keys = array_merge($keys, $r[1]); + if ($next == 0) { + break; + } + if ($server['scanmax'] > 0 && count($keys) >= $server['scanmax']) { + break; + } + } + } + sort($keys); + $namespaces = array(); // Array to hold our top namespaces. + // Build an array of nested arrays containing all our namespaces and containing keys. + foreach ($keys as $key) { + // Ignore keys that are to long (Redis supports keys that can be way to long to put in an url). + if (strlen($key) > $config['maxkeylen']) { + continue; + } -// Get all keys from Redis. -$keys = $redis->keys('*'); + $key = explode($server['seperator'], $key); //@todo: may be separator ? + if ($config['showEmptyNamespaceAsKey'] && $key[count($key) - 1] == '') { + array_pop($key); + $key[count($key) - 1] .= ':'; + } -sort($keys); + // $d will be a reference to the current namespace. + $d = &$namespaces; -$namespaces = array(); // Array to hold our top namespaces. + // We loop though all the namespaces for this key creating the array for each. + // Each time updating $d to be a reference to the last namespace so we can create the next one in it. + for ($i = 0; $i < (count($key) - 1); ++$i) { + if (!isset($d[$key[$i]])) { + $d[$key[$i]] = array(); + } -// Build an array of nested arrays containing all our namespaces and containing keys. -foreach ($keys as $key) { - // Ignore keys that are to long (Redis supports keys that can be way to long to put in an url). - if (strlen($key) > $config['maxkeylen']) { - continue; - } + $d = &$d[$key[$i]]; + } + + // Nodes containing an item named __phpredisadmin__ are also a key, not just a directory. + // This means that creating an actual key named __phpredisadmin__ will make this bug. + $d[$key[count($key) - 1]] = array('__phpredisadmin__' => true); - $key = explode($config['seperator'], $key); + // Unset $d so we don't accidentally overwrite it somewhere else. + unset($d); + } - // $d will be a reference to the current namespace. - $d = &$namespaces; + // Recursive function used to print the namespaces. + function print_namespace($item, $name, $fullkey, $islast) { + global $config, $server, $redis; + + // Is this also a key and not just a namespace? + if (isset($item['__phpredisadmin__'])) { + // Unset it so we won't loop over it when printing this namespace. + unset($item['__phpredisadmin__']); + + $class = array(); + $len = false; + + if (isset($_GET['key']) && ($fullkey == $_GET['key'])) { + $class[] = 'current'; + } + if ($islast) { + $class[] = 'last'; + } + + // Get the number of items in the key. + if (!isset($config['faster']) || !$config['faster']) { + $type = ''; + try { + $type = $redis->type($fullkey); + } catch (\Predis\Response\ServerException $th) { + $class[] = 'empty'; + } + switch ($type) { + case 'hash': + $len = $redis->hLen($fullkey); + break; + + case 'list': + $len = $redis->lLen($fullkey); + break; + + case 'set': + $len = $redis->sCard($fullkey); + break; + + case 'zset': + $len = $redis->zCard($fullkey); + break; + } + } + + if (empty($name) && $name != '0') { + $name = ''; + $class[] = 'empty'; + } + + ?> + > + + () + + 0) { + ?> +
  • +
     () + [X] +
      + $childitem) { + // $fullkey will be empty on the first call. + if ($fullkey === '') { + $childfullkey = $childname; + } else { + $childfullkey = $fullkey.$server['seperator'].$childname; + } + + print_namespace($childitem, $childname, $childfullkey, (--$l == 0)); + } + + ?> +
    +
  • + true); + $dbHasData = array_key_exists("db$d", $info['Keyspace']); - // Unset $d so we don't accidentally overwrite it somewhere else. - unset($d); -} + if (!$dbHasData && ((isset($server['hide']) && $server['hide']) || (!isset($server['hide']) && $config['hideEmptyDBs']))) { + return false; // we don't show empty dbs, so return false to tell the caller to continue the loop + } + + $dbinfo = sprintf("$prefix%'.-{$padding}d", $d); + if ($dbHasData) { + $dbinfo = sprintf("%s (%d)", $dbinfo, $info['Keyspace'][$db]['keys']); + } + $dbinfo = str_replace('.', '  ', $dbinfo); // 2 spaces per character are needed to get the alignment right + + return $dbinfo; + } + +} // if redis @@ -51,7 +180,9 @@ } else { $iframe = substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], '?') + 1); - if (strpos($iframe, '&') !== false) { + if (strpos($iframe, '//') === 0 || strpos($iframe, 'http') === 0) { + $iframe = 'overview.php'; + } else if (strpos($iframe, '&') !== false) { $iframe = substr_replace($iframe, '.php?', strpos($iframe, '&'), 1); } else { $iframe .= '.php'; @@ -60,101 +191,16 @@ - - - -// Recursive function used to print the namespaces. -function print_namespace($item, $name, $fullkey, $islast) { - global $config, $server, $redis; - - // Is this also a key and not just a namespace? - if (isset($item['__phpredisadmin__'])) { - // Unset it so we won't loop over it when printing this namespace. - unset($item['__phpredisadmin__']); - - $type = $redis->type($fullkey); - $class = array(); - $len = false; - - if (isset($_GET['key']) && ($fullkey == $_GET['key'])) { - $class[] = 'current'; - } - if ($islast) { - $class[] = 'last'; - } - - // Get the number of items in the key. - if (!isset($config['faster']) || !$config['faster']) { - switch ($type) { - case 'hash': - $len = $redis->hLen($fullkey); - break; - - case 'list': - $len = $redis->lLen($fullkey); - break; - - case 'set': - // This is currently the only way to do this, this can be slow since we need to retrieve all keys - $len = count($redis->sMembers($fullkey)); - break; - - case 'zset': - // This is currently the only way to do this, this can be slow since we need to retrieve all keys - $len = count($redis->zRange($fullkey, 0, -1)); - break; - } - } - - - ?> - > - () - - 0) { - ?> -
  • -
     () - [X] -
      - $childitem) { - // $fullkey will be empty on the first call. - if (empty($fullkey)) { - $childfullkey = $childname; - } else { - $childfullkey = $fullkey.$config['seperator'].$childname; - } - - print_namespace($childitem, $childname, $childfullkey, (--$l == 0)); - } - - ?> -
    -
  • - + +
    +
    +
    - diff --git a/info.php b/info.php index 341a0a6..77c6a3d 100644 --- a/info.php +++ b/info.php @@ -1,39 +1,30 @@ resetStat(); +if (isset($_GET['reset'])) { + $redis->config('resetstat'); header('Location: info.php'); die; } - - // Fetch the info $info = $redis->info(); $alt = false; - - - $page['css'][] = 'frame'; $page['js'][] = 'frame'; -require 'header.inc.php'; +require 'includes/header.inc.php'; ?>

    Info

    -

    -Reset usage statistics +Reset usage statistics

    - @@ -45,7 +36,7 @@ } ?> - > + > diff --git a/js/frame.js b/js/frame.js index 2fb031d..0196452 100644 --- a/js/frame.js +++ b/js/frame.js @@ -1,17 +1,14 @@ - $(function() { if (history.replaceState) { - window.parent.history.replaceState({}, '', document.location.href.replace('?', '&').replace(/([a-z]*)\.php/, '?$1')); + window.parent.history.replaceState({}, '', document.location.href.replace('?', '&').replace(/\/([a-z]*)\.php/, '/?$1')); } - $('#type').change(function(e) { $('#hkeyp' ).css('display', e.target.value == 'hash' ? 'block' : 'none'); $('#indexp').css('display', e.target.value == 'list' ? 'block' : 'none'); $('#scorep').css('display', e.target.value == 'zset' ? 'block' : 'none'); }).change(); - $('.delkey, .delval').click(function(e) { e.preventDefault(); @@ -19,7 +16,7 @@ $(function() { $.ajax({ type: "POST", url: this.href, - data: 'post=1', + data: 'post=1&csrf=' + phpRedisAdmin_csrfToken, success: function(url) { top.location.href = top.location.pathname+url; } diff --git a/js/index.js b/js/index.js index daeac11..a3f3414 100644 --- a/js/index.js +++ b/js/index.js @@ -1,45 +1,122 @@ - $(function() { - $('#sidebar a').click(function(e) { - if (e.currentTarget.href.indexOf('/?') == -1) { - return; + $('#selected_all_keys').on('click', function () { + if ($(this).html()=='Select all'){ + $('input[name=checked_keys]').each(function () { + $(this).attr('checked', 'checked'); + }); + $(this).html('Select none'); + }else { + $('input[name=checked_keys]').each(function () { + $(this).removeAttr('checked'); + }); + $(this).html('Select all'); } + }) + + $('#sidebar').on('click', 'a', function(e) { + if (e.currentTarget.className.indexOf('batch_del') !== -1) { + e.preventDefault(); + var selected_keys = []; + $('input[name=checked_keys]:checked').each(function () { + selected_keys.push($(this).val()); + }); + if (selected_keys.length == 0) { + alert('Please select the keys you want to delete.'); + return; + } + if (confirm('Are you sure you want to delete all selected keys?')) { + $.ajax({ + type: "POST", + url: this.href, + data: { + post: 1, + selected_keys: JSON.stringify(selected_keys), + csrf: phpRedisAdmin_csrfToken + }, + success: function(url) { + top.location.href = top.location.pathname+url; + } + }); + } + } else if (e.currentTarget.className.indexOf('deltree') !== -1) { + e.preventDefault(); + + if (confirm('Are you sure you want to delete this whole tree and all it\'s keys?')) { + $.ajax({ + type: "POST", + url: this.href, + data: { + post: 1, + csrf: phpRedisAdmin_csrfToken + }, + success: function(url) { + top.location.href = top.location.pathname+url; + } + }); + } + } else { + if (e.currentTarget.href.indexOf('/?') == -1) { + return; + } - e.preventDefault(); + e.preventDefault(); - var href; + var href; - if ((e.currentTarget.href.indexOf('?') == -1) || - (e.currentTarget.href.indexOf('?') == (e.currentTarget.href.length - 1))) { - href = 'overview.php'; - } else { - href = e.currentTarget.href.substr(e.currentTarget.href.indexOf('?') + 1); + if ((e.currentTarget.href.indexOf('?') == -1) || + (e.currentTarget.href.indexOf('?') == (e.currentTarget.href.length - 1))) { + href = 'overview.php'; + } else { + href = e.currentTarget.href.substr(e.currentTarget.href.indexOf('?') + 1); + + if (href.indexOf('&') != -1) { + href = href.replace('&', '.php?'); + } else { + href += '.php'; + } + } - if (href.indexOf('&') != -1) { - href = href.replace('&', '.php?'); + if (href.indexOf('flush.php') == 0) { + if (confirm('Are you sure you want to delete this key and all it\'s values?')) { + $.ajax({ + type: "POST", + url: href, + data: { + post: 1, + csrf: phpRedisAdmin_csrfToken + }, + success: function() { + window.location.reload(); + } + }); + } } else { - href += '.php'; + $('#iframe').attr('src', href); } + + $('li.current').removeClass('current'); + $(this).parent().addClass('current'); } + }); - $('#iframe').attr('src', href); + $('#server').change(function(e) { + // always show overview when switching server, only keep var s (old database index might not exist on new server) + const base = location.href.split('?', 1)[0]; + location.href = base + '?overview&s=' + e.target.value; }); - $('#server').change(function(e) { - if (location.href.indexOf('?') == -1) { - location.href = location.href+'?s='+e.target.value; - } else if (location.href.indexOf('&s=') == -1) { - location.href = location.href+'&s='+e.target.value; - } else { - location.href = location.href.replace(/s=[0-9]*/, 's='+e.target.value); - } + $('#database').change(function(e) { + // always show overview when switching db, only keep vars s and d (whatever we are doing (show/edit key) won't be valid on new db) + const base = location.href.split('?', 1)[0]; + const s = location.href.match(/s=[0-9]*/); + location.href = base + '?overview&' + s + '&d=' + e.target.value; }); $('li.current').parents('li.folder').removeClass('collapsed'); - $('li.folder').click(function(e) { + $('#sidebar').on('click', 'li.folder', function(e) { var t = $(this); if ((e.pageY >= t.offset().top) && @@ -49,15 +126,17 @@ $(function() { } }); - $('a').click(function() { - $('li.current').removeClass('current'); + $('#btn_server_filter').click(function() { + var filter = $('#server_filter').val(); + location.href = top.location.pathname + '?overview&s=' + $('#server').val() + '&d=' + ($('#database').val() || '') + '&filter=' + filter; }); - $('li a').click(function() { - $(this).parent().addClass('current'); + $('#server_filter').keydown(function(e){ + if (e.keyCode == 13) { + $('#btn_server_filter').click(); + } }); - $('#filter').focus(function() { if ($(this).hasClass('info')) { $(this).removeClass('info').val(''); @@ -85,19 +164,46 @@ $(function() { }); }); - $('.deltree').click(function(e) { + var isResizing = false; + var lastDownX = 0; + var lastWidth = 0; + + var resizeSidebar = function(w) { + $('#sidebar').css('width', w); + $('#keys').css('width', w); + $('#resize').css('left', w + 10); + $('#resize-layover').css('left', w + 15); + $('#frame').css('left', w + 15); + }; + + if (parseInt($.cookie('sidebar')) > 0) { + resizeSidebar(parseInt($.cookie('sidebar'))); + } + + $('#resize').on('mousedown', function (e) { + isResizing = true; + lastDownX = e.clientX; + lastWidth = $('#sidebar').width(); + $('#resize-layover').css('z-index', 1000); e.preventDefault(); + }); + $(document).on('mousemove', function (e) { + if (!isResizing) { + return; + } - if (confirm('Are you sure you want to delete this whole tree and all it\'s keys?')) { - $.ajax({ - type: "POST", - url: this.href, - data: 'post=1', - success: function(url) { - top.location.href = top.location.pathname+url; - } - }); + var w = lastWidth - (lastDownX - e.clientX); + if (w < 250 ) { + w = 250; + } else if (w > 1000) { + w = 1000; } + + resizeSidebar(w); + $.cookie('sidebar', w); + }).on('mouseup', function (e) { + isResizing = false; + $('#resize-layover').css('z-index', 0); }); }); diff --git a/js/jquery-cookie.js b/js/jquery-cookie.js new file mode 100644 index 0000000..29769eb --- /dev/null +++ b/js/jquery-cookie.js @@ -0,0 +1,114 @@ +/*! + * jQuery Cookie Plugin v1.4.1 + * https://github.com/carhartl/jquery-cookie + * + * Copyright 2006, 2014 Klaus Hartl + * Released under the MIT license + */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD (Register as an anonymous module) + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + + var pluses = /\+/g; + + function encode(s) { + return config.raw ? s : encodeURIComponent(s); + } + + function decode(s) { + return config.raw ? s : decodeURIComponent(s); + } + + function stringifyCookieValue(value) { + return encode(config.json ? JSON.stringify(value) : String(value)); + } + + function parseCookieValue(s) { + if (s.indexOf('"') === 0) { + // This is a quoted cookie as according to RFC2068, unescape... + s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + } + + try { + // Replace server-side written pluses with spaces. + // If we can't decode the cookie, ignore it, it's unusable. + // If we can't parse the cookie, ignore it, it's unusable. + s = decodeURIComponent(s.replace(pluses, ' ')); + return config.json ? JSON.parse(s) : s; + } catch(e) {} + } + + function read(s, converter) { + var value = config.raw ? s : parseCookieValue(s); + return $.isFunction(converter) ? converter(value) : value; + } + + var config = $.cookie = function (key, value, options) { + + // Write + + if (arguments.length > 1 && !$.isFunction(value)) { + options = $.extend({}, config.defaults, options); + + if (typeof options.expires === 'number') { + var days = options.expires, t = options.expires = new Date(); + t.setMilliseconds(t.getMilliseconds() + days * 864e+5); + } + + return (document.cookie = [ + encode(key), '=', stringifyCookieValue(value), + options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE + options.path ? '; path=' + options.path : '', + options.domain ? '; domain=' + options.domain : '', + options.secure ? '; secure' : '' + ].join('')); + } + + // Read + + var result = key ? undefined : {}, + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling $.cookie(). + cookies = document.cookie ? document.cookie.split('; ') : [], + i = 0, + l = cookies.length; + + for (; i < l; i++) { + var parts = cookies[i].split('='), + name = decode(parts.shift()), + cookie = parts.join('='); + + if (key === name) { + // If second argument (value) is a function it's a converter... + result = read(cookie, value); + break; + } + + // Prevent storing a cookie that we couldn't decode. + if (!key && (cookie = read(cookie)) !== undefined) { + result[name] = cookie; + } + } + + return result; + }; + + config.defaults = {}; + + $.removeCookie = function (key, options) { + // Must not alter options, thus extending a fresh object... + $.cookie(key, '', $.extend({}, options, { expires: -1 })); + return !$.cookie(key); + }; + +})); diff --git a/js/jquery.js b/js/jquery.js new file mode 100644 index 0000000..93adea1 --- /dev/null +++ b/js/jquery.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.2 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"":"")+""),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;e=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
    Key
    Value
    a",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="
    "+""+"
    ",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="
    t
    ",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="
    ",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function( +a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

    ";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
    ";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*",""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
    ","
    "]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f +.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(;d1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]===""&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
    ").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/login.inc.php b/login.inc.php deleted file mode 100644 index 633bd94..0000000 --- a/login.inc.php +++ /dev/null @@ -1,63 +0,0 @@ - 1, - 'nc' => 1, - 'cnonce' => 1, - 'qop' => 1, - 'username' => 1, - 'uri' => 1, - 'response' => 1 - ); - -$data = array(); -$keys = implode('|', array_keys($needed_parts)); - -preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER); - -foreach ($matches as $m) { - $data[$m[1]] = $m[3] ? $m[3] : $m[4]; - unset($needed_parts[$m[1]]); -} - -if (!empty($needed_parts)) { - header('HTTP/1.1 401 Unauthorized'); - header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); - die; -} - -if (!isset($config['login'][$data['username']])) { - header('HTTP/1.1 401 Unauthorized'); - header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); - die('Invalid username and/or password combination.'); -} - -$login = $config['login'][$data['username']]; -$login['name'] = $data['username']; - -$password = md5($login['name'].':'.$realm.':'.$login['password']); - -$response = md5($password.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.md5($_SERVER['REQUEST_METHOD'].':'.$data['uri'])); - -if ($data['response'] != $response) { - header('HTTP/1.1 401 Unauthorized'); - header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); - die('Invalid username and/or password combination.'); -} - -?> diff --git a/login.php b/login.php new file mode 100644 index 0000000..5c12301 --- /dev/null +++ b/login.php @@ -0,0 +1,45 @@ + + +

    phpRedisAdmin

    + + + + + + +
    +

    Invalid username/password

    +

    Please try again.

    +
    + + + + > + + + > + + + + + diff --git a/logout.php b/logout.php index 502d339..07e10ae 100644 --- a/logout.php +++ b/logout.php @@ -1,40 +1,56 @@ 1, - 'nc' => 1, - 'cnonce' => 1, - 'qop' => 1, - 'username' => 1, - 'uri' => 1, - 'response' => 1 - ); - -$data = array(); -$keys = implode('|', array_keys($needed_parts)); - -preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER); - -foreach ($matches as $m) { - $data[$m[1]] = $m[3] ? $m[3] : $m[4]; - unset($needed_parts[$m[1]]); -} - - -if (!isset($_GET['nonce'])) { - header('Location: logout.php?nonce='.$data['nonce']); - die; +require_once 'includes/common.inc.php'; +global $redis, $config, $csrfToken, $server; + +if (!empty($config['cookie_auth'])) { + // Cookie-based auth + setcookie('phpRedisAdminLogin', '', 1); + header("Location: login.php"); + die(); +} else if (isset($config['login_as_acl_auth'])) { + // HTTP Basic auth + header('HTTP/1.1 401 Unauthorized'); + die(''); +} else { + // HTTP Digest auth + $needed_parts = array( + 'nonce' => 1, + 'nc' => 1, + 'cnonce' => 1, + 'qop' => 1, + 'username' => 1, + 'uri' => 1, + 'response' => 1 + ); + + $data = array(); + $keys = implode('|', array_keys($needed_parts)); + + preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER); + + foreach ($matches as $m) { + $data[$m[1]] = $m[3] ? $m[3] : $m[4]; + unset($needed_parts[$m[1]]); + } + + + if (!isset($_GET['nonce'])) { + header('Location: logout.php?nonce='.$data['nonce']); + die; + } + + + if ($data['nonce'] == $_GET['nonce']) { + unset($_SERVER['PHP_AUTH_DIGEST']); + + if (!empty($config['cookie_auth'])) { + $login = authCookie(); + } else { + $login = authHttpDigest(); + } + } + + + header('Location: logout.php'); } - - -if ($data['nonce'] == $_GET['nonce']) { - unset($_SERVER['PHP_AUTH_DIGEST']); - - require 'login.inc.php'; -} - - -header('Location: '.substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'logout.php'))); - -?> diff --git a/overview.php b/overview.php index ac488a2..5c67370 100644 --- a/overview.php +++ b/overview.php @@ -1,9 +1,7 @@ auth($server['auth'])) { - die('ERROR: Authentication failed ('.$server['host'].':'.$server['port'].')'); + if (isset($config['login_as_acl_auth'])) { + // Currently only support one server at a time + if ($i > 0) { + break; + } + } else { + // Setup a connection to Redis. + if(isset($server['scheme']) && $server['scheme'] === 'unix' && $server['path']) { + $redis = new Predis\Client(array('scheme' => 'unix', 'path' => $server['path'])); + } else { + $redis = !$server['port'] ? new Predis\Client($server['host']) : new Predis\Client('tcp://'.$server['host'].':'.$server['port']); + } + try { + $redis->connect(); + } catch (Predis\CommunicationException $exception) { + $redis = false; } } - - if ($server['db'] != 0) { - if (!$redis->select($server['db'])) { - die('ERROR: Selecting database failed ('.$server['host'].':'.$server['port'].','.$server['db'].')'); - } + if(!$redis) { + $info[$i] = false; + } else { + if (isset($server['auth'])) { + if (!$redis->auth($server['auth'])) { + die('ERROR: Authentication failed ('.$server['host'].':'.$server['port'].')'); + } + } + if ($server['db'] != 0) { + if (!$redis->select($server['db'])) { + die('ERROR: Selecting database failed ('.$server['host'].':'.$server['port'].','.$server['db'].')'); + } + } + + $info[$i] = $redis->info(); + $info[$i]['size'] = $redis->dbSize(); + if (isset($config['login_as_acl_auth'])) { + $info[$i]['username'] = $redis->acl->whoami(); + } + + if (!isset($info[$i]['Server'])) { + $info[$i]['Server'] = array( + 'redis_version' => $info[$i]['redis_version'], + 'uptime_in_seconds' => $info[$i]['uptime_in_seconds'] + ); + } + if (!isset($info[$i]['Memory'])) { + $info[$i]['Memory'] = array( + 'used_memory' => $info[$i]['used_memory'] + ); + } } - $info[$i] = $redis->info(); - $info[$i]['size'] = $redis->dbSize(); } @@ -40,27 +72,48 @@ $page['css'][] = 'frame'; $page['js'][] = 'frame'; -require 'header.inc.php'; +require 'includes/header.inc.php'; ?> $server) { ?>
    -

    +

    + + +
    Server Down
    +
    - + - + + + - + + + - +
    Redis version:
    Redis version:
    Keys:
    Memory used:
    Memory used:
    Uptime:
    Uptime:
    Username:
    Last save:
    [S]
    Last save:
    + = 0) { + echo format_time(time() - $info[$i]['Persistence']['rdb_last_save_time']) . " ago"; + } else { + echo format_time(-(time() - $info[$i]['Persistence']['rdb_last_save_time'])) . "in the future"; + } + } else { + echo 'never'; + } + ?> + [S]
    + @@ -69,10 +122,10 @@

    -Redis Documentation +Redis Documentation

    diff --git a/predis b/predis deleted file mode 160000 index 6e9db69..0000000 --- a/predis +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6e9db69e729119b17510ee86e2a4d1ce3c6a1ad7 diff --git a/rename.php b/rename.php index 8264f1b..f744c48 100644 --- a/rename.php +++ b/rename.php @@ -1,9 +1,7 @@ $config['maxkeylen']) { @@ -14,15 +12,15 @@ // Refresh the top so the key tree is updated. - require 'header.inc.php'; + require 'includes/header.inc.php'; ?>

    Edit Name of

    -
    + + @@ -44,13 +43,11 @@ >

    -

    -

    diff --git a/save.php b/save.php index 56214a9..ef8b1d8 100644 --- a/save.php +++ b/save.php @@ -1,14 +1,12 @@

    Saving

    @@ -25,6 +23,6 @@ done. \ No newline at end of file diff --git a/ttl.php b/ttl.php index fd9752d..c5d7a88 100644 --- a/ttl.php +++ b/ttl.php @@ -1,32 +1,28 @@ persist($_POST['key']); } else { - $redis->setTimeout($_POST['key'], $_POST['ttl']); + $redis->expire($_POST['key'], $_POST['ttl']); } header('Location: view.php?key='.urlencode($_POST['key'])); die; } - - - $page['css'][] = 'frame'; $page['js'][] = 'frame'; -require 'header.inc.php'; +require 'includes/header.inc.php'; ?>

    Edit TTL

    -
    + +

    @@ -35,16 +31,14 @@

    -> (-1 to remove the TTL) +> (-1 to remove the TTL)

    -

    -

    diff --git a/view.php b/view.php index bb6ca35..0fdb1b5 100644 --- a/view.php +++ b/view.php @@ -1,38 +1,45 @@ Invalid key type($_GET['key']); + $exists = $redis->exists($_GET['key']); +} catch (\Predis\Response\ServerException $th) { + ?> +
    +

    getMessage() ?>

    +
    + type($_GET['key']); -$exists = $redis->exists($_GET['key']); - +$count_elements_page = isset($config['count_elements_page']) ? $config['count_elements_page'] : false; +$page_num_request = isset($_GET['page']) ? (int)$_GET['page'] : 1; +$page_num_request = $page_num_request === 0 ? 1 : $page_num_request; ?>

    - [R] - [X] - [E] + [R] + [X] + [E]

    ttl($_GET['key']); -$encoding = $redis->object('encoding', $_GET['key']); +try { + $encoding = $redis->object('encoding', $_GET['key']); +} catch (Exception $e) { + $encoding = null; +} switch ($type) { case 'string': $value = $redis->get($_GET['key']); + $value = encodeOrDecode('load', $_GET['key'], $value); $size = strlen($value); break; case 'hash': $values = $redis->hGetAll($_GET['key']); - $size = count($values); + foreach ($values as $k => $value) { + $values[$k] = encodeOrDecode('load', $_GET['key'], $value); + } + $size = count($values); + ksort($values); break; case 'list': @@ -70,26 +84,53 @@ case 'set': $values = $redis->sMembers($_GET['key']); - $size = count($values); + foreach ($values as $k => $value) { + $values[$k] = encodeOrDecode('load', $_GET['key'], $value); + } + $size = count($values); + sort($values); break; case 'zset': $values = $redis->zRange($_GET['key'], 0, -1); - $size = count($values); + foreach ($values as $k => $value) { + $values[$k] = encodeOrDecode('load', $_GET['key'], $value); + } + $size = count($values); break; + + default: + $size = -1; +} + +if (isset($values) && ($count_elements_page !== false)) { + $values = array_slice($values, $count_elements_page * ($page_num_request - 1), $count_elements_page,true); } - ?> - + + + - +
    Type:
    TTL:
    [E]
    TTL:
    [E]
    Encoding:
    Size:
    Size:
    + +
    @@ -97,15 +138,73 @@ $count_elements_page)) { + $prev = $page_num_request - 1; + $next = $page_num_request + 1; + $lastpage = ceil($size / $count_elements_page); + $lpm1 = $lastpage - 1; + $adjacents = 3; + $pagination = '
    '; + $url = preg_replace('/&page=(\d+)/i', '', $_SERVER['REQUEST_URI']); + + if ($page_num_request > 1) $pagination .= " "; else + $pagination .= "← "; + + if ($lastpage < 7 + ($adjacents * 2)) { //not enough pages to bother breaking it up + for ($counter = 1; $counter <= $lastpage; $counter++) { + if ($counter == $page_num_request) $pagination .= $page_num_request . ' '; else + $pagination .= "$counter "; + } + } elseif ($lastpage > 5 + ($adjacents * 2)) { //enough pages to hide some + + if ($page_num_request < 1 + ($adjacents * 2)) { //close to beginning; only hide later pages + for ($counter = 1; $counter < 4 + ($adjacents * 2); $counter++) { + if ($counter == $page_num_request) $pagination .= $page_num_request . ' '; else + $pagination .= "$counter "; + } + $pagination .= "... "; + $pagination .= "$lpm1 "; + $pagination .= "$lastpage "; + } elseif ($lastpage - ($adjacents * 2) > $page_num_request && $page_num_request > ($adjacents * 2)) { //in middle; hide some front and some back + $pagination .= "1 "; + $pagination .= "2 "; + $pagination .= "... "; + for ($counter = $page_num_request - $adjacents; $counter <= $page_num_request + $adjacents; $counter++) { + if ($counter == $page_num_request) $pagination .= $page_num_request . ' '; else + $pagination .= "$counter "; + } + $pagination .= "... "; + $pagination .= "$lpm1 "; + $pagination .= "$lastpage "; + } else { //close to end; only hide early pages + $pagination .= "1 "; + $pagination .= "2 "; + $pagination .= "... "; + for ($counter = $lastpage - (2 + ($adjacents * 2)); $counter <= $lastpage; $counter++) { + if ($counter == $page_num_request) $pagination .= $page_num_request . ' '; else + $pagination .= "$counter "; + } + } + } + if ($page_num_request < $counter - 1) $pagination .= " "; else + $pagination .= "→ "; + $pagination .= "
    "; +} + +if (isset($pagination)) { + echo $pagination; +} + // String if ($type == 'string') { ?> -
    - [E] +
    + [E]
    - [X] + [X]
    @@ -120,30 +219,39 @@
    Key
    Value
     
     
    $value) { ?> - >
    - [E] + >
    + [E]
    - [X] + [X]
    -lIndex($_GET['key'], $i); + $count_elements_page)) { + $start = 0; + $end = $size; + } else { + $start = $count_elements_page * ($page_num_request - 1); + $end = min($start + $count_elements_page, $size); + } + + for ($i = $start; $i < $end; ++$i) { + $value = $redis->lIndex($_GET['key'], $i); + $value = encodeOrDecode('load', $_GET['key'], $value); ?> - >> @@ -159,12 +267,12 @@ exists($value) ? ''.nl2br(format_html($value)).'' : nl2br(format_html($value)); + $display_value = $redis->exists($value) ? ''.format_html($value).'' : format_html($value); ?> - >> @@ -180,27 +288,27 @@ zScore($_GET['key'], $value); - $display_value = $redis->exists($value) ? ''.nl2br(format_html($value)).'' : nl2br(format_html($value)); + $display_value = $redis->exists($value) ? ''.format_html($value).'' : format_html($value); ?> - >>
    Index
    Value
     
     
    - [E] +
    + [E]
    - [X] + [X]
    Value
     
     
    - [E] +
    + [E]
    - [X] + [X]
    - [E] - [X] +
    + [E] + [X]

    - Add another value + Add another value