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
index daf3eea..4dc5d11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
config.inc.php
*.phar
vendor
+
+# IDEs metadata
+/nbproject/
diff --git a/Dockerfile b/Dockerfile
index ebca2d9..610dbda 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,15 @@
-FROM composer/composer
+FROM composer:2.2
+
+RUN apk add --no-cache tini tzdata
-ADD . /src/app/
WORKDIR /src/app
-RUN \
- composer install && \
- cp includes/config.environment.inc.php includes/config.inc.php
+COPY . .
-EXPOSE 80
+RUN set -xe; \
+ composer install; \
+ cp includes/config.environment.inc.php includes/config.inc.php
-ENTRYPOINT [ "php", "-S", "0.0.0.0:80" ]
+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 115bcfc..0938c08 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,5 +1,5 @@
-phpRedisAdmin 1.6.0
-===================
+phpRedisAdmin
+=============
phpRedisAdmin is a simple web interface to manage [Redis](http://redis.io/)
databases. It is released under the
@@ -39,6 +39,29 @@ cd phpRedisAdmin
git clone https://github.com/nrk/predis.git vendor
```
+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
====
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/composer.json b/composer.json
index 287c77e..3b3ab81 100644
--- a/composer.json
+++ b/composer.json
@@ -1,8 +1,8 @@
{
"name": "erik-dubbelboer/php-redis-admin",
"description": "Simple web interface to manage Redis databases.",
- "version": "1.6.0",
- "license": "CC-BY-ND",
+ "version": "1.24.0",
+ "license": "CC-BY-3.0",
"homepage": "https://github.com/ErikDubbelboer/phpRedisAdmin",
"authors": [
{
@@ -13,8 +13,11 @@
}
],
"require": {
- "predis/predis": "1.0.3"
+ "ext-mbstring": "*",
+ "ext-json": "*",
+ "predis/predis": "v2.3.0",
+ "paragonie/random_compat": ">=2"
},
"minimum-stability": "stable",
- "target-dir": "ErikDubbelboer/phpRedisAdmin"
+ "target-dir": "ErikDubbelboer/phpRedisAdmin"
}
diff --git a/composer.lock b/composer.lock
index cd0cb49..f724f31 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1,35 +1,85 @@
{
"_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#composer-lock-the-lock-file",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "hash": "e81aef935c2a6d36cf7690dbdb9d129a",
- "content-hash": "3212fc4e8463f3bf5ff7db4655eecbf0",
+ "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": "v1.0.3",
+ "version": "v2.3.0",
"source": {
"type": "git",
- "url": "https://github.com/nrk/predis.git",
- "reference": "84060b9034d756b4d79641667d7f9efe1aeb8e04"
+ "url": "https://github.com/predis/predis.git",
+ "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nrk/predis/zipball/84060b9034d756b4d79641667d7f9efe1aeb8e04",
- "reference": "84060b9034d756b4d79641667d7f9efe1aeb8e04",
+ "url": "https://api.github.com/repos/predis/predis/zipball/bac46bfdb78cd6e9c7926c697012aae740cb9ec9",
+ "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9",
"shasum": ""
},
"require": {
- "php": ">=5.3.2"
+ "php": "^7.2 || ^8.0"
},
"require-dev": {
- "phpunit/phpunit": "~4.0"
+ "friendsofphp/php-cs-fixer": "^3.3",
+ "phpstan/phpstan": "^1.9",
+ "phpunit/phpunit": "^8.0 || ^9.4"
},
"suggest": {
- "ext-curl": "Allows access to Webdis when paired with phpiredis",
- "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
+ "ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
},
"type": "library",
"autoload": {
@@ -43,27 +93,41 @@
],
"authors": [
{
- "name": "Daniele Alessandri",
- "email": "suppakilla@gmail.com",
- "homepage": "http://clorophilla.net/"
+ "name": "Till Krüss",
+ "homepage": "https://till.im/",
+ "role": "Maintainer"
}
],
- "description": "Flexible and feature-complete PHP client library for Redis",
- "homepage": "http://github.com/nrk/predis",
+ "description": "A flexible and feature-complete Redis client for PHP.",
+ "homepage": "http://github.com/predis/predis",
"keywords": [
"nosql",
"predis",
"redis"
],
- "time": "2015-07-30 18:34:15"
+ "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": [],
+ "stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
- "platform": [],
- "platform-dev": []
+ "platform": {
+ "ext-mbstring": "*",
+ "ext-json": "*"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.6.0"
}
diff --git a/css/common.css b/css/common.css
index b2a8431..9dc6276 100644
--- a/css/common.css
+++ b/css/common.css
@@ -52,6 +52,6 @@ background: url(http://github.com/images/add.png) left center no-repeat;
.data {
-white-space: pre;
+white-space: pre-wrap;
}
diff --git a/css/frame.css b/css/frame.css
index e54ca9e..7cf935d 100644
--- a/css/frame.css
+++ b/css/frame.css
@@ -14,7 +14,6 @@ margin-left: -8em;
}
form .button {
-margin-left: -7em;
}
@@ -53,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 2c8faf5..4a56aa9 100644
--- a/css/index.css
+++ b/css/index.css
@@ -3,10 +3,14 @@ position: absolute;
top: 0;
bottom: 0;
left: 0;
+display: flex;
+flex-direction: column;
width: 290px;
height: 100%;
+}
+
+#header {
padding-left: 10px;
-border-right: 1px solid #000;
}
#sidebar a, #sidebar a:visited {
@@ -23,11 +27,9 @@ text-decoration: underline;
#keys {
-position: absolute;
-top: 18.5em;
-left: 0;
-bottom: 0;
+flex-grow: 1;
width: 290px;
+margin-top: 10px;
padding-left: 10px;
overflow: auto;
}
@@ -40,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 {
@@ -114,6 +122,7 @@ background-color: #aaa;
cursor: col-resize;
padding: 0;
margin: 0;
+border-left: 1px solid #000;
}
#resize-layover {
@@ -132,11 +141,12 @@ top: 0;
left: 305px;
right: 0;
bottom: 0;
-padding-left: 2em;
}
#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 df51bee..c329919 100644
--- a/delete.php
+++ b/delete.php
@@ -1,13 +1,13 @@
+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 e50da25..998012c 100644
--- a/edit.php
+++ b/edit.php
@@ -2,8 +2,7 @@
require_once 'includes/common.inc.php';
-
-
+global $redis, $config, $csrfToken, $server;
// Are we editing or creating a new key?
$edit = false;
@@ -35,70 +34,83 @@
die('ERROR: could not encode value');
}
- // String
- if ($_POST['type'] == 'string') {
- $redis->set($key, $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($key, input_convert($_POST['hkey']))) {
- $redis->hDel($key, input_convert($_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'].')');
+ }
+
+ if ($edit && !$redis->hExists($key, input_convert($_POST['hkey']))) {
+ $redis->hDel($key, input_convert($_GET['hkey']));
+ }
+
+ $redis->hSet($key, input_convert($_POST['hkey']), $value);
}
- $redis->hSet($key, input_convert($_POST['hkey']), $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');
+ }
+ }
- // List
- else if (($_POST['type'] == 'list') && isset($_POST['index'])) {
- $size = $redis->lLen($key);
-
- if (($_POST['index'] == '') ||
- ($_POST['index'] == $size) ||
- ($_POST['index'] == -1)) {
- // Push it at the end
- $redis->rPush($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');
+ // 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);
+ }
}
- }
- // 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($key, encodeOrDecode('save', $key, input_convert($_POST['oldvalue'])));
+ $redis->zAdd($key, input_convert($_POST['score']), $value);
}
- }
- // ZSet
- else if (($_POST['type'] == 'zset') && isset($_POST['score'])) {
- // The only way to edit a ZSet value is to add it and remove the old 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 'includes/header.inc.php';
- // Refresh the top so the key tree is updated.
- require 'includes/header.inc.php';
+ ?>
+
+
-
-
+
+
getMessage() ?>
+
+
-
+?>
\ No newline at end of file
diff --git a/export.php b/export.php
index 69c15c4..d17839e 100644
--- a/export.php
+++ b/export.php
@@ -2,19 +2,24 @@
require_once 'includes/common.inc.php';
-
-
+global $redis, $config, $csrfToken, $server;
// Export to redis-cli commands
-function export_redis($key) {
+function export_redis($key, $filter = false, $transform = false) {
+
global $redis;
$type = $redis->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);
@@ -116,9 +120,12 @@ function export_json($key) {
header('Content-type: '.$ct.'; charset=utf-8');
header('Content-Disposition: inline; filename="export.'.$ext.'"');
+ $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);
}
}
}
@@ -163,7 +187,8 @@ function export_json($key) {
?>
Export
-
flushdb();
diff --git a/import.php b/import.php
index 9dbca76..5e5a89e 100644
--- a/import.php
+++ b/import.php
@@ -1,9 +1,7 @@
Import
-
$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);
- }
- }
+ if (isset($_SESSION['phpredisadmin_csrf'])) {
+ $csrfToken = $_SESSION['phpredisadmin_csrf'];
+ } else {
+ $csrfToken = bin2hex(random_bytes(16));
+ $_SESSION['phpredisadmin_csrf'] = $csrfToken;
}
-
- unset($process);
+} else {
+ $csrfToken = 'nosession';
}
-
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if ($_POST['csrf'] !== $csrfToken) {
+ die('bad csrf token');
+ }
+}
// These includes are needed by each script.
@@ -46,7 +43,6 @@
$i = 0;
}
-
if (isset($_GET['s']) && is_numeric($_GET['s']) && ($_GET['s'] < count($config['servers']))) {
$i = $_GET['s'];
}
@@ -55,10 +51,8 @@
$server['id'] = $i;
$server['charset'] = isset($server['charset']) && $server['charset'] ? $server['charset'] : false;
-
mb_internal_encoding('utf-8');
-
if (isset($login, $login['servers'])) {
if (array_search($i, $login['servers']) === false) {
die('You are not allowed to access this database.');
@@ -102,7 +96,19 @@
}
if (!isset($server['scansize'])) {
- $server['scansize'] = $config['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'])) {
@@ -111,17 +117,29 @@
}
}
+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(isset($server['scheme']) && $server['scheme'] === 'unix' && $server['path']) {
+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('tcp://'.$server['host'].':'.$server['port']);
+ $redis = !$server['port'] ? new Predis\Client($server['host']) : new Predis\Client($server['scheme'].'://'.$server['host'].':'.$server['port']);
}
try {
$redis->connect();
} catch (Predis\CommunicationException $exception) {
- $redis = false;
+ die('ERROR: ' . $exception->getMessage());
}
if (isset($server['auth'])) {
@@ -130,11 +148,12 @@
}
}
+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
index bb7c76c..06a06fe 100644
--- a/includes/config.environment.inc.php
+++ b/includes/config.environment.inc.php
@@ -2,6 +2,19 @@
include 'config.sample.inc.php';
+// get configs from environment variables
+$config['cookie_auth'] = getenv('COOKIE_AUTH') ?: false;
+$config['count_elements_page'] = getenv('COUNT_ELEMENTS_PAGE') ?: 100;
+$config['faster'] = getenv('FASTER') ?: true;
+$config['filter'] = getenv('FILTER') ?: '*';
+$config['hideEmptyDBs'] = getenv('HIDE_EMPTY_DBS') ?: false;
+$config['keys'] = getenv('KEYS') ?: false;
+$config['maxkeylen'] = getenv('MAX_KEY_LEN') ?: 100;
+$config['scansize'] = getenv('SCAN_SIZE') ?: 1000;
+$config['scanmax'] = getenv('SCAN_MAX') ?: 1000;
+$config['seperator'] = getenv('SEPERATOR') ?: ':';
+$config['showEmptyNamespaceAsKey'] = getenv('SHOW_EMPTY_NAMESPACE_AS_KEY') ?: false;
+
$admin_user = getenv('ADMIN_USER');
$admin_pass = getenv('ADMIN_PASS');
@@ -14,14 +27,22 @@
}
$i=1;
+$config['servers'] = array();
-while (TRUE) {
+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;
@@ -31,16 +52,35 @@
$server_name = $server_host;
}
- if (empty($server_port)) {
+ 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' => '*',
+ '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
index 321c8d6..923a60c 100644
--- a/includes/config.sample.inc.php
+++ b/includes/config.sample.inc.php
@@ -9,7 +9,8 @@
'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'
+ '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.
@@ -31,13 +32,17 @@
'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).
+ '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,
@@ -56,6 +61,15 @@
)
),*/
+ // 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
@@ -76,7 +90,8 @@
'keys' => false,
// How many entries to fetch using each SCAN command.
- 'scansize' => 1000
-);
+ 'scansize' => 1000,
-?>
+ // The total number of entries to fetch. Set to 0 or -1 for no limit.
+ 'scanmax' => 0
+);
diff --git a/includes/functions.inc.php b/includes/functions.inc.php
index 89368e3..589d324 100644
--- a/includes/functions.inc.php
+++ b/includes/functions.inc.php
@@ -26,39 +26,30 @@ function input_convert($str) {
}
-function format_ago($time, $ago = false) {
+function format_time($time) {
$minute = 60;
$hour = $minute * 60;
$day = $hour * 24;
$when = $time;
- if ($when >= 0)
- $suffix = 'ago';
- else {
- $when = -$when;
- $suffix = 'in the future';
- }
-
if ($when > $day) {
- $when = round($when / $day);
- $what = '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) {
- $when = round($when / $hour);
- $what = '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) {
- $when = round($when / $minute);
- $what = 'minute';
- } else {
- $what = 'second';
- }
-
- if ($when != 1) $what .= 's';
-
- if ($ago) {
- return "$when $what $suffix";
+ $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 "$when $what";
+ return sprintf("%d sec%s",$when,($when != 1) ? 's' : '');
}
}
@@ -74,6 +65,15 @@ function format_size($size) {
}
+function format_ttl($seconds) {
+ if ($seconds > 60) {
+ return sprintf('%d (%s)', $seconds, format_time($seconds));
+ } else {
+ return $seconds;
+ }
+}
+
+
function str_rand($length) {
$r = '';
@@ -101,3 +101,6 @@ function encodeOrDecode($action, $key, $data) {
return $data;
}
+function getRelativePath($base) {
+ return substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], $base));
+}
diff --git a/includes/header.inc.php b/includes/header.inc.php
index aa618f1..7679bd6 100644
--- a/includes/header.inc.php
+++ b/includes/header.inc.php
@@ -4,6 +4,7 @@
header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: private');
+header('X-Frame-Options: sameorigin');
?>
@@ -35,5 +36,9 @@
+
+
diff --git a/includes/login.inc.php b/includes/login.inc.php
index 633bd94..8cfb26d 100644
--- a/includes/login.inc.php
+++ b/includes/login.inc.php
@@ -1,63 +1,144 @@
1,
+ 'nc' => 1,
+ 'cnonce' => 1,
+ 'qop' => 1,
+ 'username' => 1,
+ 'uri' => 1,
+ 'response' => 1
+ );
-$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));
-$data = array();
-$keys = implode('|', array_keys($needed_parts));
+ preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER);
-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]]);
+ }
-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 (!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.');
+ }
-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'];
-$login = $config['login'][$data['username']];
-$login['name'] = $data['username'];
+ $password = md5($login['name'].':'.$realm.':'.$login['password']);
-$password = md5($login['name'].':'.$realm.':'.$login['password']);
+ $response = md5($password.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']));
-$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