diff --git a/.docker/release/Dockerfile b/.docker/release/Dockerfile index 97757bb44..d6675462e 100644 --- a/.docker/release/Dockerfile +++ b/.docker/release/Dockerfile @@ -1,51 +1,188 @@ +# syntax=docker/dockerfile:1 + +FROM alpine:3.18.5 AS builder +RUN apk add --no-cache php82 php82-json php-phar php-zip +RUN apk add --no-cache npm +RUN npm install -g gulp yarn +WORKDIR /source +COPY package.json yarn.lock ./ +RUN yarn install +COPY . . +# Patch release.php with hotfix from: https://github.com/xgbstar1/snappymail-docker/blob/main/Dockerfile, so that release.php doesn't fail with error +RUN sed -i 's_^if.*rename.*snappymail.v.0.0.0.*$_if (!!system("mv snappymail/v/0.0.0 snappymail/v/{$package->version}")) {_' cli/release.php || true +RUN php release.php +RUN set -eux; \ + VERSION=$( ls build/dist/releases/webmail ); \ + ls -al build/dist/releases/webmail/$VERSION/snappymail-$VERSION.tar.gz; \ + mkdir -p /snappymail; \ + tar -zxvf build/dist/releases/webmail/$VERSION/snappymail-$VERSION.tar.gz -C /snappymail; \ + find /snappymail -type d -exec chmod 550 {} \; ; \ + find /snappymail -type f -exec chmod 440 {} \; ; \ + find /snappymail/data -type d -exec chmod 750 {} \; ; \ + # Remove unneeded files + rm -v /snappymail/README.md /snappymail/_include.php + # Inspired by the original Rainloop dockerfile from youtous on GitLab -FROM php:8.1-fpm-bullseye +FROM php:8.2-fpm-alpine AS final -ARG FILES_ZIP - - - -LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm based on Debian Buster" - -ENV UID=991 GID=991 UPLOAD_MAX_SIZE=25M LOG_TO_STDERR=true MEMORY_LIMIT=128M SECURE_COOKIES=true -ENV fpm.pool.clear_env=false +LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine" # Install dependencies such as nginx -RUN mkdir -p /usr/share/man/man1/ /usr/share/man/man3/ /usr/share/man/man7/ && \ - apt-get update -q --fix-missing && \ - apt-get -y upgrade && \ - apt-get install --no-install-recommends -y \ - apt-transport-https gnupg openssl wget curl ca-certificates nginx supervisor sudo \ - unzip libzip-dev libxml2-dev libldb-dev libldap2-dev \ - sqlite3 libsqlite3-dev libsqlite3-0 libpq-dev postgresql-client mariadb-client logrotate \ - zip mlocate libpcre3-dev libicu-dev \ - build-essential chrpath libssl-dev \ - libxft-dev libfreetype6 libfreetype6-dev \ - libpng-dev libjpeg62-turbo-dev \ - libfontconfig1 libfontconfig1-dev \ - && \ - rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache ca-certificates nginx supervisor bash # Install PHP extensions -RUN php -m && \ - docker-php-ext-configure ldap --with-libdir=lib/$(uname -m)-linux-gnu/ && \ - docker-php-ext-configure intl && \ - docker-php-ext-configure gd --with-freetype --with-jpeg && \ - docker-php-ext-install ldap opcache pdo_mysql pdo_pgsql zip intl gd && \ - php -m +# apcu +RUN set -eux; \ + apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \ + pecl install apcu; \ + docker-php-ext-enable apcu; \ + docker-php-source delete; \ + apk del .build-dependencies; + +# gd +RUN set -eux; \ + apk add --no-cache freetype libjpeg-turbo libpng; \ + apk add --no-cache --virtual .deps freetype-dev libjpeg-turbo-dev libpng-dev; \ + docker-php-ext-configure gd --with-freetype --with-jpeg; \ + docker-php-ext-install gd; \ + apk del .deps + +# gmagick +# RUN set -eux; \ +# apk add --no-cache graphicsmagick libgomp; \ +# apk add --no-cache --virtual .deps graphicsmagick-dev libtool; \ +# apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \ +# pecl install gmagick-2.0.6RC1; \ +# docker-php-ext-enable gmagick; \ +# docker-php-source delete; \ +# apk del .build-dependencies; \ +# apk del .deps + +# gnupg +RUN set -eux; \ + apk add --no-cache gnupg gpgme; \ + apk add --no-cache --virtual .deps gpgme-dev; \ + apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \ + pecl install gnupg; \ + docker-php-ext-enable gnupg; \ + docker-php-source delete; \ + apk del .build-dependencies; \ + apk del .deps + +# imagick +RUN set -eux; \ + apk add --no-cache imagemagick libgomp; \ + apk add --no-cache --virtual .deps imagemagick-dev; \ + apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \ + echo | pecl install imagick; \ + docker-php-ext-enable imagick; \ + docker-php-source delete; \ + apk del .build-dependencies; \ + apk del .deps + +# intl +RUN set -eux; \ + apk add --no-cache icu-libs; \ + apk add --no-cache --virtual .deps icu-dev; \ + docker-php-ext-configure intl; \ + docker-php-ext-install intl; \ + apk del .deps + +# ldap +RUN set -eux; \ + apk add --no-cache libldap; \ + apk add --no-cache --virtual .deps openldap-dev; \ + docker-php-ext-configure ldap; \ + docker-php-ext-install ldap; \ + apk del .deps + +# mysql +RUN docker-php-ext-install pdo_mysql + +# opcache +RUN docker-php-ext-install opcache + +# postgres +RUN set -eux; \ + apk add --no-cache postgresql-libs; \ + apk add --no-cache --virtual .deps postgresql-dev; \ + docker-php-ext-install pdo_pgsql; \ + apk del .deps + +# redis +RUN set -eux; \ + apk add --no-cache liblzf zstd-libs; \ + apk add --no-cache --virtual .deps zstd-dev; \ + apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \ + pecl install igbinary; \ + docker-php-ext-enable igbinary; \ + pecl install --configureoptions 'enable-redis-igbinary="yes" enable-redis-lzf="yes" enable-redis-zstd="yes"' redis; \ + docker-php-ext-enable redis; \ + docker-php-source delete; \ + apk del .build-dependencies; \ + apk del .deps + +# tidy +RUN set -eux; \ + apk add --no-cache tidyhtml; \ + apk add --no-cache --virtual .deps tidyhtml-dev; \ + docker-php-ext-install tidy; \ + apk del .deps + +# uuid +RUN set -eux; \ + apk add --no-cache libuuid; \ + apk add --no-cache --virtual .deps util-linux-dev; \ + apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \ + pecl install uuid; \ + docker-php-ext-enable uuid; \ + docker-php-source delete; \ + apk del .build-dependencies; \ + apk del .deps + +# xxtea - Manually install php8 compatible version from https://github.com/xxtea/xxtea-pecl master branch +RUN set -eux; \ + apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \ + wget -q https://github.com/xxtea/xxtea-pecl/tarball/3f5888a29045e12301254151737c5dab4523a1c1 -O xxtea.tar; \ + echo '9cbfd9c27255767deb26ddedf69e738d401d88ac9762d82c8510f9768842ca18 xxtea.tar' | sha256sum -c -; \ + tar -C /usr/src -xvf xxtea.tar; \ + cd /usr/src/xxtea-xxtea-pecl-3f5888a; \ + phpize; \ + ./configure --with-php-config=/usr/local/bin/php-config --enable-xxtea=yes; \ + make install; \ + docker-php-ext-enable xxtea; \ + cd -; \ + rm -fv xxtea.tar; \ + rm -rfv /usr/src/xxtea*; \ + apk del .build-dependencies; + +# zip +RUN set -eux; \ + apk add --no-cache libzip; \ + apk add --no-cache --virtual .deps libzip-dev; \ + docker-php-ext-install zip; \ + apk del .deps # Install snappymail -WORKDIR /tmp -COPY ${FILES_ZIP} . -RUN mkdir /snappymail && \ - unzip -q ${FILES_ZIP} -d /snappymail && \ - find /snappymail -type d -exec chmod 755 {} \; && \ - find /snappymail -type f -exec chmod 644 {} \; && \ - rm -rf ${FILES_ZIP} +# The 'www-data' user/group in alpine is 82:82. The 'nginx' user/group in alpine is 101:101, and is part of www-data group +COPY --chown=www-data:www-data --from=builder /snappymail /snappymail +# Use a custom snappymail data folder +RUN mv -v /snappymail/data /var/lib/snappymail; +# Setup configs +COPY --chown=root:root .docker/release/files / +RUN set -eux; \ + chown www-data:www-data /snappymail/include.php; \ + chmod 440 /snappymail/include.php; \ + chmod +x /entrypoint.sh; \ + # Disable the built-in php-fpm configs, since we're using our own config + mv -v /usr/local/etc/php-fpm.d/docker.conf /usr/local/etc/php-fpm.d/docker.conf.disabled; \ + mv -v /usr/local/etc/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf.disabled; \ + mv -v /usr/local/etc/php-fpm.d/zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf.disabled; -# Install other content -COPY files / -RUN chmod +x /entrypoint.sh && chmod +x /logrotate-loop.sh -VOLUME /snappymail/data +USER root +WORKDIR /snappymail +VOLUME /var/lib/snappymail EXPOSE 8888 +EXPOSE 9000 +ENTRYPOINT [] CMD ["/entrypoint.sh"] diff --git a/.docker/release/files/entrypoint.sh b/.docker/release/files/entrypoint.sh old mode 100644 new mode 100755 index eed82bf5b..75e82d729 --- a/.docker/release/files/entrypoint.sh +++ b/.docker/release/files/entrypoint.sh @@ -1,23 +1,20 @@ #!/bin/sh +set -eu -# Create not root user -groupadd --gid "$GID" php-cli -f -adduser --uid "$UID" --disabled-password --gid "$GID" --shell /bin/bash --home /home/php-cli php-cli --force --gecos "" - +DEBUG=${DEBUG:-} +if [ "$DEBUG" = 'true' ]; then + set -x +fi +UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-25M} +MEMORY_LIMIT=${MEMORY_LIMIT:-128M} +SECURE_COOKIES=${SECURE_COOKIES:-true} # Set attachment size limit sed -i "s//$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf sed -i "s//$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf -# Set log output to STDERR if wanted (LOG_TO_STDERR=true) -if [ "$LOG_TO_STDERR" = true ]; then - echo "[INFO] Logging to stderr activated" - sed -i "s/.*error_log.*$/error_log \/dev\/stderr warn;/" /etc/nginx/nginx.conf - sed -i "s/.*error_log.*$/php_admin_value[error_log] = \/dev\/stderr/" /usr/local/etc/php-fpm.d/php-fpm.conf -fi - # Secure cookies -if [ "${SECURE_COOKIES}" = true ]; then +if [ "${SECURE_COOKIES}" = 'true' ]; then echo "[INFO] Secure cookies activated" { echo 'session.cookie_httponly = On'; @@ -26,43 +23,58 @@ if [ "${SECURE_COOKIES}" = true ]; then } > /usr/local/etc/php/conf.d/cookies.ini; fi -# Copy snappymail default config if absent -SNAPPYMAIL_CONFIG_FILE=/snappymail/data/_data_/_default_/configs/application.ini +echo "[INFO] Snappymail version: $( ls /snappymail/snappymail/v )" + +# Set permissions on snappymail data +echo "[INFO] Setting permissions on /var/lib/snappymail" +chown -R www-data:www-data /var/lib/snappymail/ +chmod 550 /var/lib/snappymail/ +find /var/lib/snappymail/ -type d -exec chmod 750 {} \; + +# Create snappymail default config if absent +SNAPPYMAIL_CONFIG_FILE=/var/lib/snappymail/_data_/_default_/configs/application.ini if [ ! -f "$SNAPPYMAIL_CONFIG_FILE" ]; then - echo "[INFO] Creating default Snappymail configuration" - mkdir -p $(dirname $SNAPPYMAIL_CONFIG_FILE) - cp /usr/local/include/application.ini $SNAPPYMAIL_CONFIG_FILE + echo "[INFO] Creating default Snappymail configuration: $SNAPPYMAIL_CONFIG_FILE" + # Run snappymail and exit. This populates the snappymail data directory and generates the config file + # On error, print php exception and exit + EXITCODE= + su - www-data -s /bin/sh -c 'php /snappymail/index.php' > /tmp/out || EXITCODE=$? + if [ -n "$EXITCODE" ]; then + cat /tmp/out + exit "$EXITCODE" + fi fi +echo "[INFO] Overriding values in snappymail configuration: $SNAPPYMAIL_CONFIG_FILE" # Enable output of snappymail logs -if [ "${LOG_TO_STDERR}" = true ]; then - sed -z 's/\; Enable logging\nenable = Off/\; Enable logging\nenable = On/' -i $SNAPPYMAIL_CONFIG_FILE - sed 's/^filename = .*/filename = "errors.log"/' -i $SNAPPYMAIL_CONFIG_FILE - sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE - sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE -else - sed -z 's/\; Enable logging\nenable = On/\; Enable logging\nenable = Off/' -i $SNAPPYMAIL_CONFIG_FILE -fi +sed '/^\; Enable logging/{ +N +s/enable = Off/enable = On/ +}' -i $SNAPPYMAIL_CONFIG_FILE +# Redirect snappymail logs to stderr /stdout +sed 's/^filename = .*/filename = "stderr"/' -i $SNAPPYMAIL_CONFIG_FILE +sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE +sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE # Always enable snappymail Auth logging sed 's/^auth_logging = .*/auth_logging = On/' -i $SNAPPYMAIL_CONFIG_FILE sed 's/^auth_logging_filename = .*/auth_logging_filename = "auth.log"/' -i $SNAPPYMAIL_CONFIG_FILE sed 's/^auth_logging_format = .*/auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"/' -i $SNAPPYMAIL_CONFIG_FILE -# Redirect snappymail logs to stderr /stdout -mkdir -p /snappymail/data/_data_/_default_/logs/ -# empty logs -cp /dev/null /snappymail/data/_data_/_default_/logs/errors.log -cp /dev/null /snappymail/data/_data_/_default_/logs/auth.log -chown -R php-cli:php-cli /snappymail/data/ +sed 's/^auth_syslog = .*/auth_syslog = Off/' -i $SNAPPYMAIL_CONFIG_FILE -# Fix permissions -chown -R $UID:$GID /snappymail/data /var/log /var/lib/nginx -chmod o+w /dev/stdout -chmod o+w /dev/stderr +( + while ! nc -vz -w 1 127.0.0.1 8888 > /dev/null 2>&1; do echo "[INFO] Checking whether nginx is alive"; sleep 1; done + while ! nc -vz -w 1 127.0.0.1 9000 > /dev/null 2>&1; do echo "[INFO] Checking whether php-fpm is alive"; sleep 1; done + # Create snappymail admin password if absent + SNAPPYMAIL_ADMIN_PASSWORD_FILE=/var/lib/snappymail/_data_/_default_/admin_password.txt + if [ ! -f "$SNAPPYMAIL_ADMIN_PASSWORD_FILE" ]; then + echo "[INFO] Creating Snappymail admin password file: $SNAPPYMAIL_ADMIN_PASSWORD_FILE" + wget -T 1 -qO- 'http://127.0.0.1:8888/?/AdminAppData/0/12345/' > /dev/null + echo "[INFO] Snappymail Admin Panel ready at http://localhost:8888/?admin. Login using password in $SNAPPYMAIL_ADMIN_PASSWORD_FILE" + fi - -# Touch supervisord PID file in order to fix permissions -touch /run/supervisord.pid -chown php-cli:php-cli /run/supervisord.pid + wget -T 1 -qO- 'http://127.0.0.1:8888/' > /dev/null + echo "[INFO] Snappymail ready at http://localhost:8888/" +) & # RUN ! -exec sudo -u php-cli -g php-cli /usr/bin/supervisord -c '/supervisor.conf' --pidfile '/run/supervisord.pid' +exec /usr/bin/supervisord -c /supervisor.conf --pidfile /run/supervisord.pid diff --git a/.docker/release/files/etc/logrotate.d/snappymail b/.docker/release/files/etc/logrotate.d/snappymail deleted file mode 100644 index 701631d98..000000000 --- a/.docker/release/files/etc/logrotate.d/snappymail +++ /dev/null @@ -1,5 +0,0 @@ -/snappymail/data/_data_/_default_/logs/* { - size 10M - rotate 0 - missingok -} \ No newline at end of file diff --git a/.docker/release/files/etc/nginx/nginx.conf b/.docker/release/files/etc/nginx/nginx.conf index 1bb643f23..ed87cb201 100644 --- a/.docker/release/files/etc/nginx/nginx.conf +++ b/.docker/release/files/etc/nginx/nginx.conf @@ -11,7 +11,7 @@ http { default_type application/octet-stream; access_log off; - error_log /tmp/ngx_error.log error; + error_log /dev/stderr error; sendfile on; keepalive_timeout 15; @@ -95,7 +95,7 @@ http { fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param HTTP_PROXY ""; fastcgi_index index.php; - fastcgi_pass unix:/tmp/php-fpm.sock; + fastcgi_pass 127.0.0.1:9000; fastcgi_intercept_errors on; fastcgi_request_buffering off; fastcgi_param REMOTE_ADDR $http_x_real_ip; diff --git a/.docker/release/files/logrotate-loop.sh b/.docker/release/files/logrotate-loop.sh deleted file mode 100644 index ba1d42469..000000000 --- a/.docker/release/files/logrotate-loop.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash -# ---------------------------------------------------------------------- -# Simple script to invoke logrotate at regular intervals -# ---------------------------------------------------------------------- - -# from https://github.com/misho-kr/docker-appliances/blob/master/nginx-nodejs/logrotate-loop.sh - -LOGROTATE_BIN="logrotate" - -STATE="$HOME/logrotate.state" -CONF="/etc/logrotate.d/snappymail" - -export LOGROTATE_BIN STATE CONF - -RUN_INTERVAL="3600" # every hour - -# helper functions for logging -export FMT="%a %b %d %Y %H:%M:%S GMT%z (%Z)" - -function log_date() { - echo "$(date +"$FMT"): $*" -} - -# ---------------------------------------------------------------------- -# Main loop of the logrotate service: -# -# while True: -# sleep N seconds -# run logrotate -# -# ---------------------------------------------------------------------- - -function logrotate_loop() { - - trap on_terminate TERM INT - - local interval="${1}" - - log_date "====================================================" - log_date - log_date "logrotate service starting (pid=$$)" - log_date "logrotate process will run every ${interval} seconds" - - while true; do - - current_time=$(date "+%s") - next_run_time=$(( current_time + interval )) - - while (( current_time < next_run_time )) - do - logrotate_sleep $(( next_run_time - current_time )) - current_time=$(date "+%s") - done - - logrotate_run - done -} - -# helper function to execute logrotate and pass it the right parameters -function logrotate_run() { - - log_date "logrotate will run now" - ${LOGROTATE_BIN} -s ${STATE} ${CONF} -} - -# ---------------------------------------------------------------------- -# Procedure to idle the execution for a number of seconds -# -# There are two requirements: -# -# - export the PID of the sleep command so that it can be terminated -# in case the logrotate service is being shutdown -# - keep this (bash) process responsive to SIGTERM while in sleep -# mode (normally the signal will be masked and will not be delivered -# until the subprocess completes) -# ---------------------------------------------------------------------- - -proc_sleep_pid="" - -function logrotate_sleep() { - - local sleep_interval=${1} - - log_date "logrotate will sleep for ${sleep_interval} seconds" - - ( exec -a "logrotate: sleep" sleep ${sleep_interval} )& - - proc_sleep_pid=$! - wait ${proc_sleep_pid} -} - -# ---------------------------------------------------------------------- -# Signal handler for logrotate service to make sure the process exits: -# -# - properly by terminating the sleep process that is used to idle -# the service -# - gracefully by writing a message in the log -# ---------------------------------------------------------------------- - -function on_terminate() { - - log_date "logrotate will terminate" - log_date - - kill -TERM ${proc_sleep_pid} - exit 0 -} - -# ---------------------------------------------------------------------- -# main -# ---------------------------------------------------------------------- - -logrotate_loop ${1:-RUN_INTERVAL} diff --git a/.docker/release/files/snappymail/include.php b/.docker/release/files/snappymail/include.php new file mode 100644 index 000000000..53ba30722 --- /dev/null +++ b/.docker/release/files/snappymail/include.php @@ -0,0 +1,3 @@ + diff --git a/.docker/release/files/supervisor.conf b/.docker/release/files/supervisor.conf index da6c6aefe..13a12a290 100644 --- a/.docker/release/files/supervisor.conf +++ b/.docker/release/files/supervisor.conf @@ -1,10 +1,13 @@ [supervisord] nodaemon=true +user=root +logfile=/dev/null +logfile_maxbytes=0 [program:nginx] command=nginx -c /etc/nginx/nginx.conf -g 'daemon off;' process_name=%(program_name)s_%(process_num)02d -user=php-cli +user=root numprocs=1 autostart=true autorestart=false @@ -17,7 +20,7 @@ stderr_logfile_maxbytes=0 [program:php-fpm] command=php-fpm -F process_name=%(program_name)s_%(process_num)02d -user=php-cli +user=root numprocs=1 autostart=true autorestart=false @@ -27,34 +30,11 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -; reads snappymail logs -[program:snappymail-auth] -command=tail -f /snappymail/data/_data_/_default_/logs/auth.log -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - -[program:snappymail-errors] -command=tail -f /snappymail/data/_data_/_default_/logs/errors.log -# everything is an error -stdout_logfile=/dev/stderr -stdout_logfile_maxbytes=0 -redirect_stderr=true - -[program:logrotate] -command=/logrotate-loop.sh -autorestart=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 - [eventlistener:subprocess-stopped] command=php /listener.php process_name=%(program_name)s_%(process_num)02d -user=php-cli +user=root numprocs=1 events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL autostart=true -autorestart=unexpected \ No newline at end of file +autorestart=unexpected diff --git a/.docker/release/files/usr/local/etc/php-fpm.d/php-fpm.conf b/.docker/release/files/usr/local/etc/php-fpm.d/php-fpm.conf index 456fdb798..fbee3d4e0 100644 --- a/.docker/release/files/usr/local/etc/php-fpm.d/php-fpm.conf +++ b/.docker/release/files/usr/local/etc/php-fpm.d/php-fpm.conf @@ -1,15 +1,21 @@ [global] daemonize = no +error_log = /dev/stderr +log_buffering = no [default] -listen = /tmp/php-fpm.sock +listen = 9000 +user = www-data +listen.owner = www-data +listen.group = www-data pm = ondemand pm.max_children = 30 pm.process_idle_timeout = 10s pm.max_requests = 500 catch_workers_output = yes +decorate_workers_output = no chdir = / -php_admin_value[error_log] = /tmp/php_error.log +pm.status_path = /status php_admin_value[log_errors] = On php_admin_value[expose_php] = Off php_admin_value[display_errors] = Off diff --git a/.docker/release/files/usr/local/include/application.ini b/.docker/release/files/usr/local/include/application.ini deleted file mode 100644 index f4bb30b77..000000000 --- a/.docker/release/files/usr/local/include/application.ini +++ /dev/null @@ -1,320 +0,0 @@ -; SnappyMail configuration file -; Please don't add custom parameters here, those will be overwritten - -[webmail] -; Text displayed as page title -title = "SnappyMail Webmail" - -; Text displayed on startup -loading_description = "SnappyMail" -favicon_url = "" -app_path = "" - -; Theme used by default -theme = "Default" - -; Allow theme selection on settings screen -allow_themes = On -allow_user_background = Off - -; Language used by default -language = "en" - -; Admin Panel interface language -language_admin = "en" - -; Allow language selection on settings screen -allow_languages_on_settings = On -allow_additional_accounts = On -allow_additional_identities = On - -; Number of messages displayed on page by default -messages_per_page = 20 - -; Mark message read after N seconds -message_read_delay = 5 - -; File size limit (MB) for file upload on compose screen -; 0 for unlimited. -attachment_size_limit = 2 - -[interface] -show_attachment_thumbnail = On - -[contacts] -; Enable contacts -enable = Off -allow_sync = Off -sync_interval = 20 -type = "sqlite" -pdo_dsn = "host=127.0.0.1;port=3306;dbname=snappymail" -pdo_user = "root" -pdo_password = "" -suggestions_limit = 30 - -[security] -custom_server_signature = "SnappyMail" -x_xss_protection_header = "1; mode=block" -openpgp = Off - -; Access settings -allow_admin_panel = On - -; Login and password for web admin panel -admin_login = "admin" -admin_password = "" -admin_totp = "" -admin_panel_host = "" -admin_panel_key = "admin" -force_https = Off -hide_x_mailer_header = On - -; For example to allow all images use "img-src https:". More info at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives -content_security_policy = "" - -; Report CSP errors to PHP and/or SnappyMail Log -csp_report = Off - -; A valid cipher method from https://php.net/openssl_get_cipher_methods -encrypt_cipher = "aes-256-cbc-hmac-sha1" - -; Strict, Lax or None -cookie_samesite = "Strict" - -; Additional allowed Sec-Fetch combinations separated by ";". -; For example: -; * Allow iframe on same domain in any mode: dest=iframe,site=same-origin -; * Allow navigate to iframe on same domain: mode=navigate,dest=iframe,site=same-origin -; * Allow navigate to iframe on (sub)domain: mode=navigate,dest=iframe,site=same-site -; * Allow navigate to iframe from any domain: mode=navigate,dest=iframe,site=cross-site -; -; Default is "site=same-origin;site=none" -secfetch_allow = "" - -[admin_panel] -allow_update = Off - -[ssl] -; Require verification of SSL certificate used. -verify_certificate = Off - -; Allow self-signed certificates. Requires verify_certificate. -allow_self_signed = On - -; https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_security_level.html -security_level = 1 - -; Location of Certificate Authority file on local filesystem (/etc/ssl/certs/ca-certificates.crt) -cafile = "" - -; capath must be a correctly hashed certificate directory. (/etc/ssl/certs/) -capath = "" - -; Location of client certificate file (pem format with private key) on local filesystem -local_cert = "" - -; This can help mitigate the CRIME attack vector. -disable_compression = On - -[capa] -quota = On - -; Allow clear folder and delete messages without moving to trash -dangerous_actions = On - -; Allow download attachments as Zip (and optionally others) -attachments_actions = On - -[login] -; If someone logs in without "@domain.tld", this value will be used -; When this value is HTTP_HOST, the $_SERVER["HTTP_HOST"] value is used. -; When this value is SERVER_NAME, the $_SERVER["SERVER_NAME"] value is used. -; When this value is gethostname, the gethostname() value is used. -; -default_domain = "" - -; Allow language selection on webmail login screen -allow_languages_on_login = On - -; Detect language from browser header `Accept-Language` -determine_user_language = On - -; Like default_domain but then HTTP_HOST/SERVER_NAME without www. -determine_user_domain = Off -login_lowercase = On - -; This option allows webmail to remember the logged in user -; once they closed the browser window. -; -; Values: -; "DefaultOff" - can be used, disabled by default; -; "DefaultOn" - can be used, enabled by default; -; "Unused" - cannot be used -sign_me_auto = "DefaultOff" - -[plugins] -; Enable plugin support -enable = Off - -; Comma-separated list of enabled plugins -enabled_list = "" - -[defaults] -; Editor mode used by default (Plain, Html) -view_editor_type = "Html" - -; layout: 0 - no preview, 1 - side preview, 2 - bottom preview -view_layout = 1 -view_use_checkboxes = On -autologout = 30 -view_html = On -show_images = Off -contacts_autosave = On -mail_use_threads = Off -allow_draft_autosave = On -mail_reply_same_folder = Off - -[logs] -; Enable logging -enable = Off - -; Path where log files will be stored -path = "" - -; Log messages of set RFC 5424 section 6.2.1 Severity level and higher (0 = highest, 7 = lowest). -; 0 = Emergency -; 1 = Alert -; 2 = Critical -; 3 = Error -; 4 = Warning -; 5 = Notice -; 6 = Informational -; 7 = Debug -level = 4 - -; Required for development purposes only. -; Disabling this option is not recommended. -hide_passwords = On -time_zone = "UTC" - -; Log filename. -; For security reasons, some characters are removed from filename. -; Allows for pattern-based folder creation (see examples below). -; -; Patterns: -; {date:Y-m-d} - Replaced by pattern-based date -; Detailed info: http://www.php.net/manual/en/function.date.php -; {user:email} - Replaced by user's email address -; If user is not logged in, value is set to "unknown" -; {user:login} - Replaced by user's login (the user part of an email) -; If user is not logged in, value is set to "unknown" -; {user:domain} - Replaced by user's domain name (the domain part of an email) -; If user is not logged in, value is set to "unknown" -; {user:uid} - Replaced by user's UID regardless of account currently used -; -; {user:ip} -; {request:ip} - Replaced by user's IP address -; -; Others: -; {imap:login} {imap:host} {imap:port} -; {smtp:login} {smtp:host} {smtp:port} -; -; Examples: -; filename = "log-{date:Y-m-d}.txt" -; filename = "{date:Y-m-d}/{user:domain}/{user:email}_{user:uid}.log" -; filename = "{user:email}-{date:Y-m-d}.txt" -; filename = "syslog" -filename = "log-{date:Y-m-d}.txt" - -; Enable auth logging in a separate file (for fail2ban) -auth_logging = Off -auth_logging_filename = "fail2ban/auth-{date:Y-m-d}.txt" -auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}" - -; Enable auth logging to syslog for fail2ban -auth_syslog = On - -[debug] -; Special option required for development purposes -enable = Off -javascript = Off -css = Off - -[cache] -; The section controls caching of the entire application. -; -; Enables caching in the system -enable = On - -; Path where cache files will be stored -path = "" - -; Additional caching key. If changed, cache is purged -index = "v1" - -; Can be: files, APCU, memcache, redis (beta) -fast_cache_driver = "files" - -; Additional caching key. If changed, fast cache is purged -fast_cache_index = "v1" - -; Browser-level cache. If enabled, caching is maintainted without using files -http = On - -; Browser-level cache time (seconds, Expires header) -http_expires = 3600 - -; Caching message UIDs when searching and sorting (threading) -server_uids = On -system_data = On - -[imap] -use_force_selection = Off -use_expunge_all_on_delete = Off -message_list_fast_simple_search = On -message_list_permanent_filter = "" -message_all_headers = Off -show_login_alert = On -fetch_new_messages = On - -[labs] -; Display message RFC 2822 date and time header, instead of the arrival internal date. -date_from_headers = On -allow_message_append = Off - -; When login fails, wait N seconds before responding -login_fault_delay = 5 -log_ajax_response_write_limit = 300 -allow_html_editor_biti_buttons = Off -allow_ctrl_enter_on_compose = On -smtp_show_server_errors = Off -sieve_auth_plain_initial = On -sieve_allow_fileinto_inbox = Off - -; PHP mail() remove To and Subject headers -mail_func_clear_headers = On - -; PHP mail() set -f emailaddress -mail_func_additional_parameters = Off -folders_spec_limit = 50 -curl_proxy = "" -curl_proxy_auth = "" -custom_login_link = "" -custom_logout_link = "" -http_client_ip_check_proxy = Off -fast_cache_memcache_host = "127.0.0.1" -fast_cache_memcache_port = 11211 -fast_cache_redis_host = "127.0.0.1" -fast_cache_redis_port = 6379 -use_local_proxy_for_external_images = On -image_exif_auto_rotate = Off -cookie_default_path = "" -cookie_default_secure = Off -replace_env_in_configuration = "" -boundary_prefix = "" -dev_email = "" -dev_password = "" - -[version] -current = "2.28.4" -saved = "Sun, 18 Dec 2022 22:10:48 +0000" diff --git a/.docker/release/test/build_and_test.sh b/.docker/release/test/build_and_test.sh new file mode 100755 index 000000000..f332ad2c2 --- /dev/null +++ b/.docker/release/test/build_and_test.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# This script uses docker buildx to build a docker image, and loads it into the docker daemon. Then it executes ./test.sh to test the docker image +# It is useful for testing release builds in development +set -eu +IMAGE=snappymail/snappymail:test +DOCKER_BUILDX=1 docker build -t "$IMAGE" -f .docker/release/Dockerfile . +.docker/release/test/test.sh "$IMAGE" diff --git a/.docker/release/test/config.yaml b/.docker/release/test/config.yaml new file mode 100644 index 000000000..47cf00ea9 --- /dev/null +++ b/.docker/release/test/config.yaml @@ -0,0 +1,29 @@ +# See: https://github.com/GoogleContainerTools/container-structure-test +# See: https://github.com/GoogleContainerTools/container-structure-test/issues/78 +schemaVersion: 2.0.0 +commandTests: + - name: Integration test + command: /bin/sh + args: + - -c + - | + set -eux + DEBUG=true /entrypoint.sh > /tmp/test 2>&1 & + sleep 5 + cat /tmp/test + pidof supervisord + pidof nginx + pidof php-fpm + ls -al /var/lib/snappymail/_data_/_default_/configs/application.ini + ls -al /var/lib/snappymail/_data_/_default_/admin_password.txt + nc -vz 127.0.0.1 8888 + nc -vz 127.0.0.1 9000 + wget -S -T 3 -O /dev/null http://127.0.0.1:8888 + kill `pidof supervisord` +metadataTest: + exposedPorts: ["8888", "9000"] + volumes: ["/var/lib/snappymail"] + entrypoint: [] + cmd: ["/entrypoint.sh"] + workdir: /snappymail + user: root diff --git a/.docker/release/test/test.sh b/.docker/release/test/test.sh new file mode 100755 index 000000000..1981cb358 --- /dev/null +++ b/.docker/release/test/test.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# This script tests a given docker image using https://github.com/GoogleContainerTools/container-structure-test +set -eu +SCRIPT_DIR=$( cd "$(dirname "$0")" && pwd ) +IMAGE=${1:-} +echo "Testing image: $IMAGE" +docker run --rm -i \ + -v /var/run/docker.sock:/var/run/docker.sock:ro \ + -v "$SCRIPT_DIR/config.yaml:/config.yaml:ro" \ + gcr.io/gcp-runtimes/container-structure-test:latest test --image "$IMAGE" --config config.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..16ef81459 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/.git +/node_modules diff --git a/.eslintrc.js b/.eslintrc.js index 06370b344..e22cb986d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,7 +3,7 @@ module.exports = { // extends: ['eslint:recommended', 'plugin:prettier/recommended'], extends: ['eslint:recommended'], parserOptions: { - ecmaVersion: 6, + ecmaVersion: 11, sourceType: 'module' }, env: { @@ -35,10 +35,17 @@ module.exports = { // vendors/bootstrap/bootstrap.native.js 'BSN': "readonly", // Mailvelope - 'mailvelope': "readonly" + 'mailvelope': "readonly", + // Punycode + 'IDN': "readonly", + // Turndown + 'TurndownService': "readonly", + // Marked + 'marked': "readonly" }, // http://eslint.org/docs/rules/ rules: { + 'no-cond-assign': 0, // plugins 'no-mixed-spaces-and-tabs': 'off', 'max-len': [ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index fc1bece3e..b37d3418e 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -community_bridge: SnappyMail +github: the-djmaze custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d53d388ed..998692964 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,8 +30,9 @@ If applicable, add screenshots to help explain your problem. - SnappyMail Version: - Mode: [e.g. standalone, nextcloud, cyberpanel, docker] -**[Debug/logging information](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)** -Place them here (few lines) or as attachments (many lines) +**Debug/logging information** +[Read here how to log](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging) +- [ ] I've placed them here (few lines) or as attachments (many lines) **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml new file mode 100644 index 000000000..af069cdf6 --- /dev/null +++ b/.github/workflows/docker-pr.yml @@ -0,0 +1,93 @@ +name: docker-pr + +on: + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + # This step generates the docker tags + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + env: + # This env var ensures {{sha}} is a real commit SHA for type=ref,event=pr + DOCKER_METADATA_PR_HEAD_SHA: 'true' + with: + images: | + djmaze/snappymail + ghcr.io/${{ github.repository }} + # type=ref,event=pr generates tag(s) on PRs only. E.g. 'pr-123', 'pr-123-abc0123' + tags: | + type=ref,event=pr + type=ref,suffix=-{{sha}},event=pr + # The rest of the org.opencontainers.image.xxx labels are dynamically generated + labels: | + org.opencontainers.image.description=SnappyMail + org.opencontainers.image.licenses=AGPLv3 + + # See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + # See: https://github.com/docker/buildx/issues/59 + - name: Build + id: build + uses: docker/build-push-action@v3 + with: + context: '.' + file: ./.docker/release/Dockerfile + platforms: linux/amd64 + push: false + load: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + - name: Docker images + run: | + docker images + + - name: Test + run: | + TAG=$( echo "${{ steps.meta.outputs.tags }}" | head -n1 ) + .docker/release/test/test.sh "$TAG" + + - name: Build all archs + uses: docker/build-push-action@v3 + with: + context: '.' + file: ./.docker/release/Dockerfile + platforms: linux/386,linux/amd64,linux/arm64 + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..b957e2fc8 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,117 @@ +name: docker + +on: + push: + tags: + - 'v2.*' + +# This is needed to push to GitHub Container Registry. See https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry +permissions: + contents: read + packages: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + # This step generates the docker tags + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + env: + # This env var ensures {{sha}} is a real commit SHA for type=ref,event=pr + DOCKER_METADATA_PR_HEAD_SHA: 'true' + with: + images: | + djmaze/snappymail + ghcr.io/${{ github.repository }} + # type=ref,event=branch generates tag(s) on branch only. E.g. 'master', 'master-abc0123' + # type=ref,event=tag generates tag(s) on tags only. E.g. 'v0.0.0', 'v0.0.0-abc0123', and 'latest' + tags: | + type=ref,event=branch + type=ref,event=tag + # The rest of the org.opencontainers.image.xxx labels are dynamically generated + labels: | + org.opencontainers.image.description=SnappyMail + org.opencontainers.image.licenses=AGPLv3 + + # See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Login to Docker Hub registry + if: startsWith(github.ref, 'refs/tags/') # Login only on tags + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + if: startsWith(github.ref, 'refs/tags/') # Login only on tags + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # See: https://github.com/docker/buildx/issues/59 + - name: Build + id: build + uses: docker/build-push-action@v3 + with: + context: '.' + file: ./.docker/release/Dockerfile + platforms: linux/amd64 + push: false + load: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + - name: Docker images + run: | + docker images + + - name: Test + run: | + TAG=$( echo "${{ steps.meta.outputs.tags }}" | head -n1 ) + .docker/release/test/test.sh "$TAG" + + - name: Build and push + id: build-and-push + uses: docker/build-push-action@v3 + with: + context: '.' + file: ./.docker/release/Dockerfile + # TODO: Add more arches? + # platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x + platforms: linux/386,linux/amd64,linux/arm64 + push: ${{ startsWith(github.ref, 'refs/tags/') }} # Push only on tags + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.htaccess b/.htaccess index faa8201c8..71041371a 100644 --- a/.htaccess +++ b/.htaccess @@ -1,7 +1,15 @@ + + AcceptPathInfo On + + RewriteEngine On # Redirect cPanel RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301] + + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.+)$ index.php/$1 [L,QSA] diff --git a/CHANGELOG.md b/CHANGELOG.md index c655d05bd..cf59a52bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,292 +1,1098 @@ +## 2.37.3 – 2024-08-27 + +### Added +- Mark images with width=1 as tracking pixel +- Show warning in Admin -> About when PHP runs in 32bit +- Edit ACL rules + [#157](https://github.com/the-djmaze/snappymail/issues/157) +- Show GnuPG version for + [#1560](https://github.com/the-djmaze/snappymail/issues/1560) +- Make sure only scalar values are allowed in $_ENV for + [#1560](https://github.com/the-djmaze/snappymail/issues/1560) +- Change minimum new mail check interval + [#1678](https://github.com/the-djmaze/snappymail/issues/1678) +- Sieve editor does not support "index" extension + [#1709](https://github.com/the-djmaze/snappymail/issues/1709) + +### Changed +- Improved domain autoconfig interaction +- MS autodiscover priorities DNS over subdomain +- Simplify sieve scripts list + [#1675](https://github.com/the-djmaze/snappymail/issues/1675) +- Handling of (token) errors due to + [#1706](https://github.com/the-djmaze/snappymail/issues/1706) +- Sabre/Xml to v4.0.5 +- Update Chinese by @Artoria2e5 +- Update French by @hguilbert + +### Fixed +- Thread sorting visible after disabling the imap capability + [#1574](https://github.com/the-djmaze/snappymail/issues/1574) +- Creating new message impossible as long as a draft exists? + [#1710](https://github.com/the-djmaze/snappymail/issues/1710) +- InvalidToken error at login + [#1706](https://github.com/the-djmaze/snappymail/issues/1706) + +### Nextcloud +- Force Nextcloud personal language by default + [#1428](https://github.com/the-djmaze/snappymail/issues/1428) + + +## 2.37.2 – 2024-08-13 + +### Added +- Validate Fetch JSON response + +### Fixed +- PATH_INFO bug due to Office365 OAuth login +- Prevent logout loop on error + +### Nextcloud +- Failed loading due to Office365 OAuth2 attempt + [#1703](https://github.com/the-djmaze/snappymail/issues/1703) + + +## 2.37.1 – 2024-08-12 + +### Fixed +- Gulp v5 broke the fonts + +### Nextcloud +- Support v30 + + +## 2.37.0 – 2024-08-12 + +### Added +- JavaScript event `rl-vm-visible` +- Detailed error message on account switch failure for + [#1594](https://github.com/the-djmaze/snappymail/issues/1594) +- Workarounds for Microsoft OAuth2 (currently requires Apache AcceptPathInfo) + [#1645](https://github.com/the-djmaze/snappymail/issues/1645) +- Support "mark for deletion" + [#1657](https://github.com/the-djmaze/snappymail/issues/1657) by @smsoft-ru +- Invoke "Update Identity" pop up right after login (when not initialized) + [#1689](https://github.com/the-djmaze/snappymail/issues/1689) +- Keyboard shortcut for "Swap default (background) color" + [#1690](https://github.com/the-djmaze/snappymail/issues/1690) + +### Changed +- Updated gulp to v5 +- Replaced vulnerable gulp-header with gulp-append-prepend +- Removed abandoned vulnerable rollup-plugin-html +- Align save button in admin security settings +- Made registerProtocolHandler('mailto') optional by activating at Settings -> General +- Improved InvalidToken handling for + [#1653](https://github.com/the-djmaze/snappymail/issues/1653) +- Cleanup localizations +- Update French by @hguilbert +- Update German by @tkasch +- Update Polish by @tinola +- Update Portuguese by @ner00 + +### Fixed +- prevent multiple afterShow() and afterHide() due to `transitionend` on multiple CSS properties +- Attempt to read property "smimeSigned" on null +- Refreshing mail list doesn't update current message + [#1654](https://github.com/the-djmaze/snappymail/issues/1654) +- Deletion of CACHE folder causing error + [#1660](https://github.com/the-djmaze/snappymail/issues/1660) +- Multiple line breaks are not displayed + [#1666](https://github.com/the-djmaze/snappymail/issues/1666) +- RainLoop\Exceptions\ClientException::__construct(): Argument #2 ($oPrevious) must be of type ?Throwable, string given + [#1686](https://github.com/the-djmaze/snappymail/issues/1686) +- SpamAssassin Division by zero + [#1694](https://github.com/the-djmaze/snappymail/issues/1694) +- Failed to parse RFC 2822 date '6 Jul 2024 16:42:09 +0200' + [#1694](https://github.com/the-djmaze/snappymail/issues/1694) +- Fix capabilities when THREAD is disabled + [#1698](https://github.com/the-djmaze/snappymail/pull/1698) by @akhil1508 + +### Nextcloud +- Failed loading due to incorrect `app_path` +- Bugfix language detection +- Allow multi-account in nc with oauth login + [#1699](https://github.com/the-djmaze/snappymail/pull/1699) by @akhil1508 + + +## 2.36.4 – 2024-06-25 + +### Added +- Customize private key passphrase expiration interval + [#1545](https://github.com/the-djmaze/snappymail/discussions/1545) +- AdvancedSearch support for filtering mails before a given date + [#1606](https://github.com/the-djmaze/snappymail/pull/1606) by @codiflow +- Control valid spam and virus headers + [#1607](https://github.com/the-djmaze/snappymail/issues/1607) +- Remember S/MIME private Key without function + [#1611](https://github.com/the-djmaze/snappymail/issues/1611) +- Resize compose dialog +- Magnetic theme + [#1637](https://github.com/the-djmaze/snappymail/pull/1637) by @TheCuteFoxxy + +### Changed +- Improved signing messages by allowing to choose between the options +- Improved language detection code +- More detailed Decrypt errors +- Update French by @hguilbert +- Update Polish by @tinola +- Update Portuguese by @ner00 +- Update Spanish by @huloza + +### Fixed +- Default language error +- Undefined $sEmail in DoAdminDomainMatch +- Handling Autocrypt header failed on `=` + [#1608](https://github.com/the-djmaze/snappymail/issues/1608) +- Blank lines are inserted when editing draft + [#1609](https://github.com/the-djmaze/snappymail/issues/1609) +- Workaround Cyrus MAILBOXID bug (disable OBJECTID capability by default due to impact) + [#1640](https://github.com/the-djmaze/snappymail/issues/1640) +- Workaround HTML with multiple body elements or MIME with multiple text/html + [#1641](https://github.com/the-djmaze/snappymail/issues/1641) + +### Nextcloud +- OIDC stay logged in + [#1620](https://github.com/the-djmaze/snappymail/pull/1620) by @avinash-0007 + + +## 2.36.3 – 2024-05-27 + +### Changed +- UserAuth prevent plugin errors (like the Nextcloud plugin did) + +### Fixed +- Undefined variable $aTokenData + [#1567](https://github.com/the-djmaze/snappymail/issues/1567) + + +## 2.36.2 – 2024-05-26 + +### Added +- "copy to" action in menu's for + [#1559](https://github.com/the-djmaze/snappymail/issues/1559) +- Log signal info for + [#1569](https://github.com/the-djmaze/snappymail/issues/1569) +- OpenPGP.js automatically import backup keys from server + +### Changed +- Improved "remember me" cookie handling +- Update Basque by @Thadah +- Update Portuguese by @ner00 + +### Fixed +- "Account already exists" + [#1561](https://github.com/the-djmaze/snappymail/issues/1561) +- Properly escape path separator in tar.php file list regex + [#1562](https://github.com/the-djmaze/snappymail/pull/1562) by @sevmonster +- Prevent mkdir() error + [#1565](https://github.com/the-djmaze/snappymail/issues/1565) +- SCRAM Exception when trying to log in to SMTP + [#1575](https://github.com/the-djmaze/snappymail/issues/1575) +- Error when redirected back to instance after Gmail OAuth + [#1580](https://github.com/the-djmaze/snappymail/issues/1580) +- Uncaught TypeError: hasPublicKeyForEmails(...).then is not a function + [#1589](https://github.com/the-djmaze/snappymail/issues/1589) +- Undefined variable $sFilename +- GPG/PGP exec() return false handling + +### Nextcloud +- OIDC login active again + [#1572](https://github.com/the-djmaze/snappymail/pull/1572) by @avinash-0007 + + +## 2.36.1 – 2024-04-23 + +### Added +- Autoconfig detect through DNS SRV (RFC 6186 & 8314) and disable MX +- Have I Been Pwned class to check breached passwords and email addresses +- Handle RFC 5987 in Content-Disposition header +- Ignore text/x-amp-html +- Show SMTP error to user + [#1521](https://github.com/the-djmaze/snappymail/issues/1521) +- OAuth2 for login using gmail (and others) + +### Changed +- logMask all AUTHENTICATE requests +- ErrorTip use white-space: pre +- Simplify LoginProcess handling +- ES2020 everywhere (require Safari 13.1) +- Modified Squire to be more in line with v2.2.8 +- CSS set min-width for .attachmentParent and .flagParent to line them up +- cPanel use extension login-cpanel instead of login-remote +- Improved login credentials handling +- Speedup Knockout a bit +- Update Belarusian by @spoooyders +- Update Chinese by @mayswind +- Update French by @hguilbert +- Update Polish by @tinola +- Update Portuguese by @ner00 + +### Fixed +- Content encoding and type detection in JavaScript could fail due to case-sensitivity. +- Extensions set logger failed +- GnuPG check open_basedir and if shell_exec is disabled + [#1385](https://github.com/the-djmaze/snappymail/issues/1385) + [#1496](https://github.com/the-djmaze/snappymail/issues/1496) + [#1555](https://github.com/the-djmaze/snappymail/issues/1555) +- Hide pagination when search result has no messages +- Prevent mbstring error before setup.php +- Prevent MessagesPerPage Infinity + [#1540](https://github.com/the-djmaze/snappymail/issues/1540) +- Reseal CryptKey failed + [#1543](https://github.com/the-djmaze/snappymail/issues/1543) + +### Nextcloud +- Add an occ command to set up the login settings + [#1552](https://github.com/the-djmaze/snappymail/issues/1552) + + +## 2.36.0 – 2024-03-18 + +### Added +- Allow setting the supported THREAD algorithm +- Icon to system folders +- Remove remembered password after 15 minutes of inactivity + [#1142](https://github.com/the-djmaze/snappymail/issues/1142) +- Swap background and text color for unreadable text on dark background + [#1486](https://github.com/the-djmaze/snappymail/issues/1486) +- Generate TOTP code at ?Admin -> Security + [#1501](https://github.com/the-djmaze/snappymail/issues/1501) +- Button to change S/MIME private key passphrase + [#1505](https://github.com/the-djmaze/snappymail/issues/1505) +- Belarusian + [#1512](https://github.com/the-djmaze/snappymail/pull/1512) by @spoooyders +- Log some domain idn_to_ascii issues + [#1513](https://github.com/the-djmaze/snappymail/issues/1513) + +### Changed +- On folder/mailbox rename, also rename all children instead of reloading all +- Seal MainAccount CryptKey and on error ask old login passphrase to reseal key. +- Moved cache drivers outside core into extensions +- Sieve always allow fileinto INBOX + [#1510](https://github.com/the-djmaze/snappymail/issues/1510) +- Moved application.ini `sieve_auth_plain_initial` to per domain config +- Languages use rfc5646, by using the shortest ISO 639 code by default +- Update French by @hguilbert +- Update Portuguese by @ner00 + +### Fixed +- On folder/mailbox rename, the old fullName must be removed from cache +- On folder/mailbox rename, the checkable option was not renamed +- Sort accounts drag & drop +- S/MIME encrypted and opaque signed not visible + [#1450](https://github.com/the-djmaze/snappymail/issues/1450) +- Wrong last UID of thread + [#1507](https://github.com/the-djmaze/snappymail/issues/1507) +- Creation of dynamic property SnappyMail\DAV\Client::$HTTP + [#1509](https://github.com/the-djmaze/snappymail/issues/1509) +- "Download as ZIP" fails for messages + [#1514](https://github.com/the-djmaze/snappymail/issues/1514) +- SMTP "Authentication failed" when IMAP uses `shortLogin` and SMTP not + [#1517](https://github.com/the-djmaze/snappymail/issues/1517) + + +## 2.35.4 – 2024-03-16 + +### Added +- \SnappyMail\IDN::toAscii() + +### Changed +- OpenPGP.js to v5.11.1 +- punycode.js lowercase domain names +- application.ini `login_lowercase` removed and now configurable per domain JSON `lowerLogin` +- Update Portuguese by @ner00 + +### Fixed +- Raise JS TypeEroor "toLowerCase" after update + [#1491](https://github.com/the-djmaze/snappymail/issues/1491) +- Call to undefined function shell_exec + [#1496](https://github.com/the-djmaze/snappymail/issues/1496) +- Download attachments as ZIP doesn't work for PGP encrypted mail + [#1499](https://github.com/the-djmaze/snappymail/issues/1499) +- Importing or downloading a PGP public key attachment from a PGP encrypted message doesn't work + [#1500](https://github.com/the-djmaze/snappymail/issues/1500) +- VCard PHP Notice: Undefined index: ENCODING + +### Nextcloud +- Changed stored password handling +- Can't login from nextcloud with 2.35.3 bug Nextcloud + [#1490](https://github.com/the-djmaze/snappymail/issues/1490) + + +## 2.35.3 – 2024-03-12 + +### Added +- GnuPG can be disabled +- Missing strings for localization inside identity popup (Cryptography > S/MIME) + [#1458](https://github.com/the-djmaze/snappymail/issues/1458) +- Automatically verify PGP and S/MIME signed messages +- TNEFDecoder for + [#1012](https://github.com/the-djmaze/snappymail/discussions/1012) +- RTF to HTML converter for + [#1012](https://github.com/the-djmaze/snappymail/discussions/1012) +- Polyfill for PHP ctype + [#1250](https://github.com/the-djmaze/snappymail/issues/1250) + +### Changed +- `new Error()` to `Error()` +- Reduce KnockoutJS footprint by removing unused code +- CSS reposition rainloopErrorTip location +- Improved error handling on PGP and S/MIME decrypt +- Improved OpenPGP.js import keys +- Use Identity S/MIME key and certificate from server instead of POST +- application.ini `[webmail]language_admin` to `[admin_panel]language` +- application.ini `[security]admin_panel_host` to `[admin_panel]host` +- application.ini `[security]admin_panel_key` to `[admin_panel]key` +- Drop deprecated Domain::SetConfig() +- Internationalized domain names are now handled as punycode +- Cacher->Get() can now return NULL +- Update French by @hguilbert +- Update Polish by @tinola +- Update Portuguese by @ner00 + +### Fixed +- Handling of Internationalized Domain Names in several areas +- Decrypt error message +- Stalwart ManageSieve Error 352 when getting Filters + [#1455](https://github.com/the-djmaze/snappymail/issues/1455) +- Nextcloud V25+ theme slightly broken + [#1463](https://github.com/the-djmaze/snappymail/issues/1463) +- PGP decryption fails with "Not armored text" + [#1462](https://github.com/the-djmaze/snappymail/issues/1462) +- AUTH_BASIC falling through as AUTH_BEARER; change AUTH_BEARER to a different value + [#1461](https://github.com/the-djmaze/snappymail/issues/1461) +- SetPassword expects \SnappyMail\SensitiveString +- Crash on importing corrupt OpenPGP keys +- Crash on old browsers instead of showing error +- Ignore popups on logoutReload() + [#1467](https://github.com/the-djmaze/snappymail/issues/1467) +- Custom SASLMechanisms fail in IMAP when the connection is secure + [#1484](https://github.com/the-djmaze/snappymail/pull/1484) by @botsarenthuman + + +## 2.35.2 – 2024-02-27 + +### Added +- GnuPG error handling +- Missing strings for localization inside identity popup (Cryptography > S/MIME) + [#1458](https://github.com/the-djmaze/snappymail/issues/1458) + +### Changed +- Update Portuguese by @ner00 + +### Fixed +- Drop support for gnupg PECL extension as it fails with "no passphrase" issues +- Error 352 when getting Filters + [#1455](https://github.com/the-djmaze/snappymail/issues/1455) + +### Nextcloud +- SetPassword(): Argument #1 must be of type SensitiveString, string given + [#1456](https://github.com/the-djmaze/snappymail/issues/1456) + + +## 2.35.1 – 2024-02-26 + +### Added +- Search functionality in Admin -> Config +- Cache S/MIME passphrases when "remember" is checked +- Import S/MIME certificate popup +- pre-verify S/MIME opaque signed messages so we have a body to view +- Sort PGP keys and S/MIME certificates on email address +- Optionally use existing private key to generate S/MIME certificate + +### Changed +- Better handling to detect which PGP or S/MIME sign/encrypt to use +- Improved StorageType handling +- Cleanup and improved Capa handling +- OPEN_PGP should be OPENPGP as it is one word +- Use get_debug_type() instead of gettype() +- Require OpenSSL due to S/MIME +- AbstractProvider::IsActive() is now an abstract method and must be defined in child class +- Make better use of SnappyMail\SensitiveString +- Update Polish translation by @tinola + +### Fixed +- Verify S/MIME signatures got broken allong the way while implementing this +- Generate S/MIME self-signed certificate failed to keep existing private key +- MIME parser RegExp didn't escape boundary which caused issues +- TypeError: b64Encode(...).match(...) is null on saving compose draft +- Fix timestampToString() for future dates + + +## 2.35.0 – 2024-02-20 + +### Added +- S/MIME support + [#259](https://github.com/the-djmaze/snappymail/issues/259) + +### Changed +- Disable IMAP METADATA by default (hardly used) +- Update Polish translation by @tinola +- Rename CSS .openpgp-control to .crypto-control +- Renamed some methods in PHP + +### Fixed +- When moving a folder/mailbox check for parent delimiter +- Mask `passphrase` in the logs for PHP < 8.2 +- Added some missing translations +- Sign messages using PGP +- Check for CONDSTORE or QRESYNC to get the HIGHESTMODSEQ +- Unable to login on certain IMAP server since 2.34.2 + [#1438](https://github.com/the-djmaze/snappymail/issues/1438) + +### Nextcloud +- Save as .eml + [#1425](https://github.com/the-djmaze/snappymail/issues/1425) + + +## 2.34.2 – 2024-02-14 + +### Fixed +- Message was sent but not saved to sent items folder + [#1432](https://github.com/the-djmaze/snappymail/issues/1432) +- Login with scram failed + [#1433](https://github.com/the-djmaze/snappymail/issues/1433) + + +## 2.34.1 – 2024-02-13 + +### Added +- Autocrypt support + [#342](https://github.com/the-djmaze/snappymail/issues/342) +- Load the mailboxes/folders of all namespaces (other users, global, shared, etc.) +- Load keys from server into OpenPGP.js + [#973](https://github.com/the-djmaze/snappymail/issues/973) +- Import PGP Keys from remote key servers +- Sort Inbox Folders with Unread Messages First + [#1427](https://github.com/the-djmaze/snappymail/issues/1427) +- Define JMAP FolderModel.myRights +- Identity Management: add identity display name + [#1405](https://github.com/the-djmaze/snappymail/issues/1405) +- Identity Management: add per-identity "sent" folder + [#1404](https://github.com/the-djmaze/snappymail/issues/1404) +- Some support for JSON-LD / Structured Email + [#1422](https://github.com/the-djmaze/snappymail/issues/1422) +- Domain Autoconfig and Microsoft's autodiscover (and also as extension/plugin) +- View MMS messages that are received via email + [#1294](https://github.com/the-djmaze/snappymail/issues/1294) +- Draft code for S/MIME + [#259](https://github.com/the-djmaze/snappymail/issues/259) + +### Changed +- Many OpenPGP improvements + [#89](https://github.com/the-djmaze/snappymail/issues/89) +- Allow CSP connect-src CORS for keys.openpgp.org to directly fetch PGP keys +- Improved handling of visible folders +- KnockoutJS Replace some ko.exportSymbol('*') in favour of ko['*'] +- KnockoutJS use Symbol for isObservableArray() +- Simplify generating folderListVisible +- Drop the bSearchSecretWords param from logger +- Transparent background for text + [#1412](https://github.com/the-djmaze/snappymail/issues/1412) +- Enable OpenPGP.js by default at install +- Added folder edit popup for improved IMAP ACL Support + [#157](https://github.com/the-djmaze/snappymail/issues/157) +- Process all IMAP namespaces +- Update Polish by @tinola +- Update Portuguese by @ner00 + +### Fixed +- Make time_zone a select list due to PEBKAC +- Workaround Outlook generated double spacing + [#1415](https://github.com/the-djmaze/snappymail/issues/1415) +- HTML Parser is not picking up the full Unsubscribe URL in the attached text file + [#1225](https://github.com/the-djmaze/snappymail/issues/1225) +- Contacts - it auto "Select All", after entry delete + [#1411](https://github.com/the-djmaze/snappymail/issues/1411) +- Message header parsing issue + [#1403](https://github.com/the-djmaze/snappymail/issues/1403) +- apple-touch-icon should not be transparent + [#1408](https://github.com/the-djmaze/snappymail/issues/1408) +- Creation of dynamic property is deprecated + [#1409](https://github.com/the-djmaze/snappymail/issues/1409) +- Ask/send readReceipt was broken +- OpenPGP public key can not be removed anymore after importing private key of subkey + [#1384](https://github.com/the-djmaze/snappymail/issues/1384) +- KnockoutJS failed to output text '0' +- JavaScript friendlySize() failed on 0 +- Workaround Dovecot `PREAUTH [CAPABILITY (null)]` issue +- Workaround disabled ACL could cause "Disconnected: Too many invalid IMAP commands" + +### Nextcloud +- Save multiple as .eml + [#1425](https://github.com/the-djmaze/snappymail/issues/1425) +- Disabled support for Nextcloud OpenID Connect + [#1420](https://github.com/the-djmaze/snappymail/issues/1420) + + +## 2.33.0 – 2024-01-22 + +### Added +- Feature to use the SQLite AddressBook per login account instead of global (on by default). +- Return all fetched messages headers in JSON. + +### Changed +- Docker hub use Alpine linux 3.18.5 and PHP 8.2 +- Some InvalidArgumentException to the better suited ValueError +- Removed some unused KnockoutJS code +- KnockoutJS drop unused rateLimit method +- Cleanup some data-bind="" +- Drop the disabled KnockoutJS twoWayBindings +- Drop support for KnockoutJS _ko_property_writers and for two-way binding they must be observables +- Login form use method="POST" to prevent uri exposure when javascript fails +- Merge code to generate MIME PGP parts and MIME Plain parts +- SMTP sendRequestWithCheck for future support of RFC's +- Cleanup mime header handling + +### Fixed +- Sorting not supported since 2.32.0 + [#1373](https://github.com/the-djmaze/snappymail/issues/1373) +- FILE_ON_SAVING_ERROR is not defined + [#1379](https://github.com/the-djmaze/snappymail/issues/1379) +- Saving EML files with same subject result in only saving latest email + [#1381](https://github.com/the-djmaze/snappymail/issues/1381) +- Some Sieve parser issues +- Handling of RainLoop Sieve script +- Sieve rfc5429 RejectCommand and ErejectCommand +- KnockoutJS title:value was removed, use attr:{title:value} +- dataBaseUpgrade() always runs on sqlite and pgsql +- Message was sent but not saved to sent items folder + [#1397](https://github.com/the-djmaze/snappymail/issues/1397) +- DKIM `pass` detection sometimes failed + + +## 2.32.0 – 2023-12-26 + +### Added +- Run full GetUids() in background when message_list_limit is set +- MessageListThreadsMap as background task when message_list_limit is set +- Properly set CACHEDIR.TAG +- Sending group email to all contact addresses + [#1286](https://github.com/the-djmaze/snappymail/pull/1286) by @rezaei92 + +### Changed +- Default IMAP message_list_limit to 10000 +- DoMessageCopy() return toFolder hash/etag +- Improved Squire WYSIWYG +- Sort real attachments and inline attachments for + [#1360](https://github.com/the-djmaze/snappymail/issues/1360) +- Nextcloud Theme fixes and improvements + [#1363](https://github.com/the-djmaze/snappymail/pull/1363) by @hampoelz +- Improve display of attachments + [#1361](https://github.com/the-djmaze/snappymail/issues/1361) +- Rename messageVisibility to messageVisible +- All CSS font-size to % instead of px +- Flip source code view of .eml attachments + [#1332](https://github.com/the-djmaze/snappymail/issues/1332) + +### Fixed +- Folders array_filter(): Argument 1 must be of type array, null given +- At upgrade set `static` and `themes` folder to 0755 +- Preview tooltip shows "null" when PREVIEW capability is disabled + +### Nextcloud +- Improved language handling + [#1362](https://github.com/the-djmaze/snappymail/pull/1362) by @avinash-0007 +- FilterLanguage had wrong parameter order +- Use NextcloudV25+ theme by default + + +## 2.31.0 – 2023-12-08 + +### Added +- PHP Hook `filter.language` to allow remote language selection + +### Changed +- Cleaner language detection +- Get Squire in sync with v2.2.5 and some bugfixes +- Update French by @Cwpute +- Squire: drop support for iPod + +### Fixed +- Call to undefined method FolderMyRights() + [#1344](https://github.com/the-djmaze/snappymail/issues/1344) +- NO Mailbox does not exist, or must be subscribed to") + [#1354](https://github.com/the-djmaze/snappymail/issues/1354) +- Flag indicators are added to wrong message + [#1347](https://github.com/the-djmaze/snappymail/pull/1347) by @SergeyMosin +- Squire: issue when using the enter key in a reply window + [#1296](https://github.com/the-djmaze/snappymail/issues/1296) +- Squire: crash on cut/delete range + +### Nextcloud +- Use language as defined in Nextcloud settings + [#1293](https://github.com/the-djmaze/snappymail/issues/1293) +- Plugin Call to undefined method RainLoop\Model\MainAccount::ImapConnectAndLoginHelper() +- SnappyMail failed due to Nextcloud Symfony polyfill + + +## 2.30.0 – 2023-12-04 + +### Added +- SnappyMail\SensitiveString class to secure passwords +- Allow to disable all IMAP features through Admin -> Domain +- Setting to open mails in a tab or new window + [#951](https://github.com/the-djmaze/snappymail/issues/951) +- Fully support IMAP PREVIEW + [#1338](https://github.com/the-djmaze/snappymail/issues/1338) +- Disable "Mark message as read after", offer manual toggle + [#1289](https://github.com/the-djmaze/snappymail/issues/1289) +- A "Move to" button inside message view as an icon/button and in the drop down menu. + [#1295](https://github.com/the-djmaze/snappymail/issues/1295) +- Support for IMAP WITHIN +- Support \noinferiors to disallow creating subfolders +- A test due to Failed loading libs.min.js + [#358](https://github.com/the-djmaze/snappymail/issues/358), + [#862](https://github.com/the-djmaze/snappymail/issues/862), + [#890](https://github.com/the-djmaze/snappymail/issues/890), + [#895](https://github.com/the-djmaze/snappymail/issues/895), + [#1238](https://github.com/the-djmaze/snappymail/issues/1238), + [#1320](https://github.com/the-djmaze/snappymail/issues/1320) + +### Changed +- Split PHP 8 polyfills from include.php +- Disable snappymail/v/0.0.0/static/.htaccess for now as many servers have issues with it +- Merged all Domain `disable_*` settings into `disabled_capabilities:[]` +- Prioritize LIST-EXTENDED over LSUB (LSUB deprecated in IMAP4rev2) +- Removed unused ImapClient::IsSupported() +- Removed obsolete `$_ENV['SNAPPYMAIL_NEXTCLOUD']` +- Removed unused Plugin->replaceTemplate() +- Removed openDropdownTrigger + +### Fixed +- Move to button does not work + [#1328](https://github.com/the-djmaze/snappymail/issues/1328) +- Mark passwords as sensitive information + [#1343](https://github.com/the-djmaze/snappymail/issues/1343) +- Account sSmtpPassword wrong value +- SCRAM sign-in failed + [#1245](https://github.com/the-djmaze/snappymail/issues/1245) +- Squire generates to many `

` + [#1339](https://github.com/the-djmaze/snappymail/issues/1339) +- Creation of dynamic property SnappyMail\Stream\ZipEntry::$compression is deprecated +- `json.after-*` hooks didn't send $aResponse as recursive array +- Sieve: Move to folder with trailing space does not work + [#1329](https://github.com/the-djmaze/snappymail/issues/1329) +- Squire: cantFocusEmptyTextNodes var is always undefined + [#1337](https://github.com/the-djmaze/snappymail/issues/1337) +- Squire: Remove redundant after replacing styles +- Squire: Handle empty nodes in moveRangeBoundariesDownTree +- Theme "Nextcloud V25+" can't be translated + [#1331](https://github.com/the-djmaze/snappymail/issues/1331) + + +## 2.29.4 – 2023-11-21 + +### Fixed +- Contacts not work + [#1319](https://github.com/the-djmaze/snappymail/issues/1319) + + +## 2.29.3 – 2023-11-21 + +### Added +- Docker Hub image + [#965](https://github.com/the-djmaze/snappymail/pull/965) by @leojonathanoh + +### Changed +- Sabre/VObject 4.5.4 and Sabre/Xml 4.0.4 + [#1311](https://github.com/the-djmaze/snappymail/issues/1311) + +### Fixed +- '#/mailbox/folder/mUID/search' uri/route handling + [#1301](https://github.com/the-djmaze/snappymail/pull/1301) by @SergeyMosin +- "Remember me" doesn't work when browser is closed + [#1313](https://github.com/the-djmaze/snappymail/issues/1313) +- Blank email displayed when "Prefer HTML to plain text" is unchecked and the message is html only + [#1302](https://github.com/the-djmaze/snappymail/issues/1302) +- Parent folder of Sub folder not useable. + [#1008](https://github.com/the-djmaze/snappymail/issues/1008) +- Large detailed header don't display body + [#1284](https://github.com/the-djmaze/snappymail/issues/1284) + +### Nextcloud +- Improvements for Install / update issues #929 + [#929](https://github.com/the-djmaze/snappymail/issues/929) +- Should use language as defined in cloud settings #1293 + [#1293](https://github.com/the-djmaze/snappymail/issues/1293) + + +## 2.29.2 – 2023-11-14 + +### Added +- Show size of folders in folders list #1303 + [#1303](https://github.com/the-djmaze/snappymail/issues/1303) + +### Fixed +- Configuration failed when using special chars in MySQL password #1308 + [#1308](https://github.com/the-djmaze/snappymail/issues/1308) +- With email open, "delete" doesn't delete #1274 + [#1274](https://github.com/the-djmaze/snappymail/issues/1274) +- Fix threading view in Thunderbird (others?) + [#1304](https://github.com/the-djmaze/snappymail/pull/1304) by @tkasch + + +## 2.29.1 – 2023-10-02 + +### Fixed +- Some small messages list bugs + + +## 2.29.0 – 2023-10-02 + +### Added +- Modern UI / Nextcloud Theme + [#629](https://github.com/the-djmaze/snappymail/pull/629) by @hampoelz +- "Add/Edit signature" label to PopupsIdentity.html + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin +- use calendar icon in message list for messages with '.ics' or 'text/calendar' attachments by @SergeyMosin + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) +- Show unseen message count when the message list is threaded + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin +- in mobile mode hide folders(left) panel when a folder is clicked + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin +- spellcheck the subject when 'allowSpellcheck' setting is true + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin +- 'collapse_blockquotes', 'allow_spellcheck' and 'mail_list_grouped' to admin settings ('defaults' section) + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin +- Browser support for autocompleting TOTP code + [#1251](https://github.com/the-djmaze/snappymail/issues/1251) + +### Changed +- URL strip tracking for + [#1225](https://github.com/the-djmaze/snappymail/issues/1225) +- Color picker use color blind palette "Tableau 10" by Maureen Stone by default + [#1199](https://github.com/the-djmaze/snappymail/issues/1199) +- Draft code to improve mobile breakpoints + [#1150](https://github.com/the-djmaze/snappymail/issues/1150) +- address input: space character can trigger '_parseValue' if the email address looks complete + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin +- if applicable set '\\answered' or '$forwarded' flag after a message is sent so the proper icon is shown in the message list view + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin + +### Fixed +- CHARSET is not valid in UTF8 mode + [#1230](https://github.com/the-djmaze/snappymail/issues/1230) +- Spam score is always "acceptable" + [#1228](https://github.com/the-djmaze/snappymail/issues/1228) +- Undefined constant PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT + [#1205](https://github.com/the-djmaze/snappymail/issues/1205) +- Fetch controller.abort(reason) handling + [#1220](https://github.com/the-djmaze/snappymail/issues/1220) +- "Request failed" on message move + [#1220](https://github.com/the-djmaze/snappymail/issues/1220) +- Unwrapped text nodes attached to squire._root + [#1234](https://github.com/the-djmaze/snappymail/pull/1234) by @SergeyMosin +- Extra wrapper div is added in Squire every time a Draft is open (or closed) after save. + [#1208](https://github.com/the-djmaze/snappymail/issues/1208) +- foreach() argument must be of type array|object + [#1237](https://github.com/the-djmaze/snappymail/issues/1237) +- `` tag 'style' is lost in replies + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin +- unseen indicator is not shown in thread view when 'listGrouped' settings is false + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin +- TOTP plugin is dependent on ctype + [#1250](https://github.com/the-djmaze/snappymail/issues/1250) + +### Nextcloud +- iFrame mode: click on unified search result opens inner iFrame + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin +- set 'smremember' cookie if 'sign_me_auto' is set to 'DefaultOn' when using 'snappymail-autologin*', otherwise nextcloud users need to re-login when the browser is re-opened + [#1248](https://github.com/the-djmaze/snappymail/pull/1248) by @SergeyMosin +- Improve UX of "Put in Calendar" option in plugin + [#1259](https://github.com/the-djmaze/snappymail/pull/1259) by @theronakpatel + + ## 2.28.4 – 2023-07-10 -## Added -- application.ini msg_default_action by @SergeyMosin - [#1204](https://github.com/the-djmaze/snappymail/pull/1204) -- application.ini view_show_next_message by @SergeyMosin - [#1204](https://github.com/the-djmaze/snappymail/pull/1204) -- application.ini view_images by @SergeyMosin - [#1204](https://github.com/the-djmaze/snappymail/pull/1204) -- nextcloud add ability to include custom php file in InstallStep migration by @SergeyMosin - [#1197](https://github.com/the-djmaze/snappymail/pull/1197) +### Added +- application.ini msg_default_action + [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin +- application.ini view_show_next_message + [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin +- application.ini view_images + [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin +- nextcloud add ability to include custom php file in InstallStep migration + [#1197](https://github.com/the-djmaze/snappymail/pull/1197) by @SergeyMosin - Support plugin for Squire editor - [#1192](https://github.com/the-djmaze/snappymail/pull/1192) + [#1192](https://github.com/the-djmaze/snappymail/issues/1192) -## Changed +### Changed - only show 'Add "domain.tld" as an application for mailto links?' message after login (firefox shows the message on every reload otherwise). - [#1204](https://github.com/the-djmaze/snappymail/pull/1204) + [#1204](https://github.com/the-djmaze/snappymail/issues/1204) - Convert getPdoAccessData() : array to a RainLoop\Pdo\Settings object instance -- New bidi buttons to Squire editor by @rezaei92 - [#1200](https://github.com/the-djmaze/snappymail/pull/1200) +- New bidi buttons to Squire editor + [#1200](https://github.com/the-djmaze/snappymail/pull/1200) by @rezaei92 -## Fixed +### Fixed - Undefined constant PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT - [#1205](https://github.com/the-djmaze/snappymail/pull/1205) -- 'reloadTime' function result is passed into 'setInterval' instead of the function by @SergeyMosin - [#1204](https://github.com/the-djmaze/snappymail/pull/1204) + [#1205](https://github.com/the-djmaze/snappymail/issues/1205) +- 'reloadTime' function result is passed into 'setInterval' instead of the function + [#1204](https://github.com/the-djmaze/snappymail/pull/1204) by @SergeyMosin - UNKNOWN-CTE Invalid data in MIME part - [#1186](https://github.com/the-djmaze/snappymail/pull/1186) + [#1186](https://github.com/the-djmaze/snappymail/issues/1186) ## 2.28.3 – 2023-06-22 -## Added +### Added - Attachments in "new window" view - [#1166](https://github.com/the-djmaze/snappymail/pull/1166) + [#1166](https://github.com/the-djmaze/snappymail/issues/1166) -## Changed +### Changed - Update Portuguese by @ner00 - Update French by @hguilbert -## Fixed +### Fixed - Some emails with inline CSS break the UI - [#1187](https://github.com/the-djmaze/snappymail/pull/1187) + [#1187](https://github.com/the-djmaze/snappymail/issues/1187) - Remote.get() Promise broken by previous change - [#1185](https://github.com/the-djmaze/snappymail/pull/1185) + [#1185](https://github.com/the-djmaze/snappymail/issues/1185) - Class "MailSo\Base\Exceptions\InvalidArgumentException" not found - [#1182](https://github.com/the-djmaze/snappymail/pull/1182) + [#1182](https://github.com/the-djmaze/snappymail/issues/1182) - First account not showed in the right list (dropbox) - [#1180](https://github.com/the-djmaze/snappymail/pull/1180) + [#1180](https://github.com/the-djmaze/snappymail/issues/1180) ## 2.28.2 – 2023-06-19 -## Added +### Added - Detailed error for "Cannot access the repository at the moment" - [#1164](https://github.com/the-djmaze/snappymail/pull/1164) + [#1164](https://github.com/the-djmaze/snappymail/issues/1164) - Bidi in Squire editor - [#1158](https://github.com/the-djmaze/snappymail/pull/1158) + [#1158](https://github.com/the-djmaze/snappymail/issues/1158) - Translate Squire UI - Nextcloud 27 compatibility by @LarsBel - JWT class for handling JSON Web Tokens -## Changed +### Changed - Update German by @cm-schl - Update French by @hguilbert - Update Polish by @tinola - Merge handling of local Account Settings. Found while investigating - [#1170](https://github.com/the-djmaze/snappymail/pull/1170) + [#1170](https://github.com/the-djmaze/snappymail/issues/1170) - Image max-width now 100% instead of 90vw -## Fixed +### Fixed - Cannot modify header information - [#929](https://github.com/the-djmaze/snappymail/pull/929) (comment) + [#929](https://github.com/the-djmaze/snappymail/issues/929) (comment) - Admin Panel broken when admin_panel_host is set - [#1169](https://github.com/the-djmaze/snappymail/pull/1169) + [#1169](https://github.com/the-djmaze/snappymail/issues/1169) - Invalid CSP report-uri - Prevent MessageList multiple request at the same time - [#1071](https://github.com/the-djmaze/snappymail/pull/1071) + [#1071](https://github.com/the-djmaze/snappymail/issues/1071) - Error in Addressbook Sync - [#1179](https://github.com/the-djmaze/snappymail/pull/1179) + [#1179](https://github.com/the-djmaze/snappymail/issues/1179) - base64_decode() second parameter must be true ## 2.28.1 – 2023-06-05 -## Changed +### Changed - Optical issue with input fields for mail and folder search - [#1149](https://github.com/the-djmaze/snappymail/pull/1149) -- Update Chinese translation by @mayswind - [#1157](https://github.com/the-djmaze/snappymail/pull/1157) -- Update Polish translation by @tinola - [#1156](https://github.com/the-djmaze/snappymail/pull/1156) + [#1149](https://github.com/the-djmaze/snappymail/issues/1149) +- Update Chinese translation + [#1157](https://github.com/the-djmaze/snappymail/pull/1157) by @mayswind +- Update Polish translation + [#1156](https://github.com/the-djmaze/snappymail/pull/1156) by @tinola -## Fixed +### Fixed - Undefined SIG constants - [#1147](https://github.com/the-djmaze/snappymail/pull/1147) + [#1147](https://github.com/the-djmaze/snappymail/issues/1147) ## 2.28.0 – 2023-05-30 -## Added +### Added - Threaded view make number orange when unread sub-messages - [#1028](https://github.com/the-djmaze/snappymail/pull/1028) + [#1028](https://github.com/the-djmaze/snappymail/issues/1028) - Handle PHP pctnl messages - addEventListener('rl-view-model') missing for Settings - [#1013](https://github.com/the-djmaze/snappymail/pull/1013) + [#1013](https://github.com/the-djmaze/snappymail/issues/1013) - CSS `--btn-border-radius` -## Changed +### Changed - Improved RTL languages support - [#1056](https://github.com/the-djmaze/snappymail/pull/1056) + [#1056](https://github.com/the-djmaze/snappymail/issues/1056) - Composer text/attachments as tabs - [#1119](https://github.com/the-djmaze/snappymail/pull/1119) + [#1119](https://github.com/the-djmaze/snappymail/issues/1119) - Filter dialog doesn't refer to folder names consistently - [#1111](https://github.com/the-djmaze/snappymail/pull/1111) + [#1111](https://github.com/the-djmaze/snappymail/issues/1111) - TLS connection for MYSQL contact db - [#1078](https://github.com/the-djmaze/snappymail/pull/1078) + [#1078](https://github.com/the-djmaze/snappymail/issues/1078) - Allow empty message body when there are attachments - [#1052](https://github.com/the-djmaze/snappymail/pull/1052) + [#1052](https://github.com/the-djmaze/snappymail/issues/1052) - PHP inherit logger as Trait -- Update Portuguese by @ner00 - [#1124](https://github.com/the-djmaze/snappymail/pull/1124) -- Update Traditional Chinese (Taiwan) by @chiyi4488 - [#1107](https://github.com/the-djmaze/snappymail/pull/1107) -- Update Russian by @konkere - [#1108](https://github.com/the-djmaze/snappymail/pull/1108) -- Update Italian by @cm-schl - [#1094](https://github.com/the-djmaze/snappymail/pull/1094) -- Update French by @hguilbert - [#1102](https://github.com/the-djmaze/snappymail/pull/1102) -- Update German by @cm-schl - [#1087](https://github.com/the-djmaze/snappymail/pull/1087) +- Update Portuguese + [#1124](https://github.com/the-djmaze/snappymail/pull/1124) by @ner00 +- Update Traditional Chinese (Taiwan) + [#1107](https://github.com/the-djmaze/snappymail/pull/1107) by @chiyi4488 +- Update Russian + [#1108](https://github.com/the-djmaze/snappymail/pull/1108) by @konkere +- Update Italian + [#1094](https://github.com/the-djmaze/snappymail/pull/1094) by @cm-schl +- Update French + [#1102](https://github.com/the-djmaze/snappymail/pull/1102) by @hguilbert +- Update German + [#1087](https://github.com/the-djmaze/snappymail/pull/1087) by @cm-schl -## Fixed +### Fixed - Show messagelist timeout/abort error for - [#1071](https://github.com/the-djmaze/snappymail/pull/1071) + [#1071](https://github.com/the-djmaze/snappymail/issues/1071) - DesktopNotifications setting not saved - [#1137](https://github.com/the-djmaze/snappymail/pull/1137) + [#1137](https://github.com/the-djmaze/snappymail/issues/1137) - PHP Deprecation warning for $_openPipes - [#1141](https://github.com/the-djmaze/snappymail/pull/1141) + [#1141](https://github.com/the-djmaze/snappymail/issues/1141) - Images size wrong - [#1134](https://github.com/the-djmaze/snappymail/pull/1134) + [#1134](https://github.com/the-djmaze/snappymail/issues/1134) - Unable to preview body of encrypted mail in mailvelope reply-to - [#1130](https://github.com/the-djmaze/snappymail/pull/1130) + [#1130](https://github.com/the-djmaze/snappymail/issues/1130) - Replace `
diff --git a/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php b/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php index 08fbfd809..8835da06c 100644 --- a/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php +++ b/integrations/owncloud/snappymail/lib/Util/SnappyMailHelper.php @@ -78,11 +78,15 @@ class SnappyMailHelper // $oDomain = \RainLoop\Model\Domain::fromIniArray('owncloud', []); $oDomain = new \RainLoop\Model\Domain('owncloud'); $iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE; - $oDomain->SetConfig( - 'localhost', 143, $iSecurityType, true, - true, 'localhost', 4190, $iSecurityType, - 'localhost', 25, $iSecurityType, true, true, false, false, - ''); + $oDomain->ImapSettings()->host = 'localhost'; + $oDomain->ImapSettings()->type = $iSecurityType; + $oDomain->ImapSettings()->shortLogin = true; + $oDomain->SieveSettings()->enabled = true; + $oDomain->SieveSettings()->host = 'localhost'; + $oDomain->SieveSettings()->type = $iSecurityType; + $oDomain->SmtpSettings()->host = 'localhost'; + $oDomain->SmtpSettings()->type = $iSecurityType; + $oDomain->SmtpSettings()->shortLogin = true; $oProvider->Save($oDomain); if (!$oConfig->Get('login', 'default_domain', '')) { $oConfig->Set('login', 'default_domain', 'owncloud'); @@ -137,11 +141,7 @@ class SnappyMailHelper */ if ($doLogin && $aCredentials[1] && $aCredentials[2]) { $oActions->Logger()->AddSecret($aCredentials[2]); - $oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2], false); - if ($oAccount) { - $oActions->Plugins()->RunHook('login.success', array($oAccount)); - $oActions->SetAuthToken($oAccount); - } + $oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]); } } } catch (\Throwable $e) { diff --git a/integrations/virtualmin/snappymail.pl b/integrations/virtualmin/snappymail.pl index 2ce72af4b..646185e81 100644 --- a/integrations/virtualmin/snappymail.pl +++ b/integrations/virtualmin/snappymail.pl @@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a # script_snappymail_versions() sub script_snappymail_versions { -return ( "2.28.4" ); +return ( "2.37.3" ); } sub script_snappymail_version_desc diff --git a/package.json b/package.json index 9e7f6b8ac..0ba9b480c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "title": "SnappyMail", "description": "Simple, modern & fast web-based email client", "private": true, - "version": "2.28.4", + "version": "2.37.3", "homepage": "https://snappymail.eu", "author": { "name": "DJ Maze", @@ -15,8 +15,7 @@ "url": "git://github.com/the-djmaze/snappymail.git" }, "scripts": { - "watch-css": "gulp watchCss", - "watch-js": "webpack --color --watch" + "watch-css": "gulp watchCss" }, "license": "SEE LICENSE IN LICENSE", "licenses": [ @@ -47,7 +46,8 @@ "babel-eslint": "^10.1.0", "del": "^6.0.0", "eslint": "^7.32.0", - "gulp": "^4.0.2", + "gulp": "^5.0.0", + "gulp-append-prepend": "^1.0.9", "gulp-cached": "^1.1.1", "gulp-clean-css": "^4.3.0", "gulp-concat": "^2.6.1", @@ -56,7 +56,6 @@ "gulp-expect-file": "^2.0.0", "gulp-filter": "^6.0.0", "gulp-group-css-media-queries": "^1.2.2", - "gulp-header": "^2.0.9", "gulp-less": "^5.0.0", "gulp-rename": "^2.0.0", "gulp-replace": "^1.1.3", @@ -65,7 +64,6 @@ "gulp-terser": "^2.1.0", "rollup": "^2.56.3", "rollup-plugin-external-globals": "^0.6.1", - "rollup-plugin-html": "^0.2.1", "rollup-plugin-includepaths": "^0.2.4", "rollup-plugin-terser": "^7.0.2" } diff --git a/plugins/README.md b/plugins/README.md index e594453a0..dcd129e09 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -1,3 +1,5 @@ +Also see https://github.com/the-djmaze/snappymail/tree/master/plugins/example + PHP ```php class Plugin extends \RainLoop\Plugins\AbstractPlugin @@ -130,8 +132,9 @@ $Plugin->addHook('hook.name', 'functionName'); ### login.credentials params: string &$sEmail - string &$sLogin + string &$sImapUser string &$sPassword + string &$sSmtpUser ### login.success params: @@ -164,6 +167,12 @@ $Plugin->addHook('hook.name', 'functionName'); bool $bSuccess \MailSo\Imap\Settings $oSettings +### imap.message-headers + params: + array &$aHeaders + + Allows you to fetch more MIME headers for messages. + ## Sieve ### sieve.before-connect @@ -285,6 +294,13 @@ and called in JavaScript using rl.pluginRemoteRequest(). params: array &$aPaths +### filter.language + params: + string &$sLanguage + bool $bAdmin + + Allows you to set a different language + ### filter.message-html params: \RainLoop\Model\Account $oAccount @@ -396,35 +412,54 @@ and called in JavaScript using rl.pluginRemoteRequest(). string $sName mixed &$mResult -### service.app-delay-start-begin - no params - -### service.app-delay-start-end - no params - # JavaScript Events ## mailbox ### mailbox.inbox-unread-count ### mailbox.message-list.selector.go-up ### mailbox.message-list.selector.go-down + ### mailbox.message.show + Use to show a specific message. +``` JavaScript + dispatchEvent( + new CustomEvent( + 'mailbox.message.show', + { + detail: { + folder: 'INBOX', + uid: 1 + }, + cancelable: false + } + ) + ); +``` + ## audio ### audio.start ### audio.stop ### audio.api.stop ## Misc -### idle ### rl-layout + event.detail value is one of: + 0. NoPreview + 1. SidePreview + 2. BottomPreview ### rl-view-model.create event.detail = the ViewModel class - Happens immediately after the ViewModel constructor + Happens immediately after the ViewModel constructor. + See accessible properties as https://github.com/the-djmaze/snappymail/blob/master/dev/Knoin/AbstractViews.js ### rl-view-model event.detail = the ViewModel class Happens after the full build (vm.onBuild()) and contains viewModelDom +### rl-vm-visible + event.detail = the ViewModel class + Happens after the model is made visible (vm.afterShow()) + ### sm-admin-login event.detail = FormData cancelable using preventDefault() diff --git a/plugins/attachments-force-open/extension.js b/plugins/attachments-force-open/extension.js new file mode 100644 index 000000000..8020ca6fa --- /dev/null +++ b/plugins/attachments-force-open/extension.js @@ -0,0 +1,13 @@ +(() => { + +const dom = document.getElementById('MailMessageView').content; + +dom.querySelector('.attachmentsControls').dataset.bind = ''; + +let ds = dom.querySelector('.attachmentsPlace').dataset; +ds.bind = ds.bind.replace('showAttachmentControls', 'true'); + +ds = dom.querySelector('.controls-handle').dataset; +ds.bind = ds.bind.replace('allowAttachmentControls', 'false'); + +})(); diff --git a/plugins/attachments-force-open/index.php b/plugins/attachments-force-open/index.php new file mode 100644 index 000000000..4110b7894 --- /dev/null +++ b/plugins/attachments-force-open/index.php @@ -0,0 +1,20 @@ +addJs('extension.js'); // add js file + } +} diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js index 4fd61a924..464c4f1cc 100644 --- a/plugins/avatars/avatars.js +++ b/plugins/avatars/avatars.js @@ -39,11 +39,15 @@ avatars = new Map, ncAvatars = new Map, templateId = 'MailMessageView', - getAvatarUid = msg => { - let from = msg.from[0], - bimi = 'pass' == from.dkimStatus ? 1 : 0; - return `${bimi}/${from.email.toLowerCase()}`; + getBimiSelector = msg => { + // Get 's' value out of 'v=BIMI1; s=foo;' + let bimiSelector = msg.headers().valueByName('BIMI-Selector'); + bimiSelector = bimiSelector ? bimiSelector.match(/;.*s=([^\s;]+)/)[1] : ''; + return bimiSelector || ''; }, + getBimiId = msg => ('pass' == msg.from[0].dkimStatus ? 1 : 0) + '-' + getBimiSelector(msg), + getAvatarUrl = msg => `?Avatar/${getBimiId(msg)}/${msg.avatar}`, + getAvatarUid = msg => `${getBimiId(msg)}/${msg.from[0].email.toLowerCase()}`, getAvatar = msg => ncAvatars.get(msg.from[0].email.toLowerCase()) || avatars.get(getAvatarUid(msg)), hash = async txt => { if (/^[0-9a-f]{15,}$/i.test(txt)) { @@ -72,6 +76,8 @@ if (rl.pluginSettingsGet('avatars', 'delay')) { queue.push([msg, fn]); runQueue(); + } else if (msg.avatar) { + fn(getAvatarUrl(msg)); } }, runQueue = (() => { @@ -84,6 +90,8 @@ item[1](url); item = queue.shift(); continue; + } else if (item[0].avatar) { + item[1](getAvatarUrl(item[0])); } else if (!avatars.has(uid)) { let from = item[0].from[0]; rl.pluginRemoteRequest((iError, data) => { @@ -97,9 +105,9 @@ runQueue(); }, 'Avatar', { bimi: 'pass' == from.dkimStatus ? 1 : 0, + bimiSelector: getBimiSelector(item[0]), email: from.email }); - break; } } runQueue(); @@ -107,75 +115,85 @@ } }).debounce(1000); - /** - * Loads images from Nextcloud contacts - */ - addEventListener('DOMContentLoaded', () => { +// addEventListener('DOMContentLoaded', () => { + /** + * Modify templates + */ + getEl('MailMessageList').content.querySelectorAll('.messageCheckbox') + .forEach(el => el.append(Element.fromHTML(``))); + const messageItemHeader = getEl(templateId).content.querySelector('.messageItemHeader'); + if (messageItemHeader) { + messageItemHeader.prepend(Element.fromHTML( + `` + )); + } + + /** + * Loads images from Nextcloud contacts + */ // rl.pluginSettingsGet('avatars', 'nextcloud'); - if (parent.OC) { - const OC = () => parent.OC, + if (parent.OC?.requestToken) { + const OC = parent.OC, nsDAV = 'DAV:', nsNC = 'http://nextcloud.com/ns', nsCard = 'urn:ietf:params:xml:ns:carddav', getElementsByTagName = (parent, namespace, localName) => parent.getElementsByTagNameNS(namespace, localName), getElementValue = (parent, namespace, localName) => - getElementsByTagName(parent, namespace, localName)?.item(0)?.textContent, - generateUrl = path => OC().webroot + '/remote.php' + path; - if (OC().requestToken) { - fetch(generateUrl(`/dav/addressbooks/users/${OC().currentUser}/contacts/`), { - mode: 'same-origin', - cache: 'no-cache', - redirect: 'error', - credentials: 'same-origin', - method: 'REPORT', - headers: { - requesttoken: OC().requestToken, - 'Content-Type': 'application/xml; charset=utf-8', - Depth: 1 - }, - body: '' - }) - .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) - .then(text => { - const - xmlParser = new DOMParser(), - responseList = getElementsByTagName( - xmlParser.parseFromString(text, 'application/xml').documentElement, - nsDAV, - 'response'); - for (let i = 0; i < responseList.length; ++i) { - const item = responseList.item(i); - if (1 == getElementValue(item, nsNC, 'has-photo')) { - [...getElementValue(item, nsCard, 'address-data').matchAll(/EMAIL.*?:([^@\r\n]+@[^@\r\n]+)/g)] - .forEach(match => { - ncAvatars.set( - match[1].toLowerCase(), - getElementValue(item, nsDAV, 'href') + '?photo' - ); - }); - } + getElementsByTagName(parent, namespace, localName)?.item(0)?.textContent; + fetch(`${OC.webroot}/remote.php/dav/addressbooks/users/${OC.currentUser}/contacts/`, { + mode: 'same-origin', + cache: 'no-cache', + redirect: 'error', + credentials: 'same-origin', + method: 'REPORT', + headers: { + requesttoken: OC.requestToken, + 'Content-Type': 'application/xml; charset=utf-8', + Depth: 1 + }, + body: '' + }) + .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) + .then(text => { + const + xmlParser = new DOMParser(), + responseList = getElementsByTagName( + xmlParser.parseFromString(text, 'application/xml').documentElement, + nsDAV, + 'response'); + for (let i = 0; i < responseList.length; ++i) { + const item = responseList.item(i); + if (1 == getElementValue(item, nsNC, 'has-photo')) { + [...getElementValue(item, nsCard, 'address-data').matchAll(/EMAIL.*?:([^@\r\n]+@[^@\r\n]+)/g)] + .forEach(match => { + ncAvatars.set( + match[1].toLowerCase(), + getElementValue(item, nsDAV, 'href') + '?photo' + ); + }); } - }); - } + } + }); } - }); +// }); + /** + * Used by MailMessageList + */ ko.bindingHandlers.fromPic = { init: (element, self, dummy, msg) => { try { if (msg?.from?.[0]) { let url = getAvatar(msg), - from = msg.from[0], fn = url=>{element.src = url}; + element.onerror = ()=>{ + element.onerror = null; + setIdenticon(msg.from[0], fn); + }; if (url) { fn(url); - } else if (msg.avatar) { - if (msg.avatar?.startsWith('data:')) { - fn(msg.avatar); - } else { - element.onerror = () => setIdenticon(from, fn); - fn(`?Avatar/${'pass' == from.dkimStatus ? 1 : 0}/${msg.avatar}`); - } + } else if (msg.avatar?.startsWith('data:')) { + fn(msg.avatar); } else { addQueue(msg, fn); } @@ -188,17 +206,6 @@ addEventListener('rl-view-model.create', e => { if (templateId === e.detail.viewModelTemplateID) { - - const - template = getEl(templateId), - messageItemHeader = template.content.querySelector('.messageItemHeader'); - - if (messageItemHeader) { - messageItemHeader.prepend(Element.fromHTML( - `` - )); - } - let view = e.detail; view.viewUserPic = ko.observable(''); view.viewUserPicVisible = ko.observable(false); @@ -214,8 +221,7 @@ if (url) { fn(url); } else if (msg.avatar) { - fn(msg.avatar.startsWith('data:') ? msg.avatar - : `?Avatar/${'pass' == msg.from[0].dkimStatus ? 1 : 0}/${msg.avatar}`); + fn(msg.avatar.startsWith('data:') ? msg.avatar : getAvatarUrl(msg)); } else { // let from = msg.from[0]; // view.viewUserPic(`?Avatar/${'pass' == from.dkimStatus ? 1 : 0}/${encodeURIComponent(from.email)}`); @@ -225,11 +231,6 @@ } }); } - - if ('MailMessageList' === e.detail.viewModelTemplateID) { - getEl('MailMessageList').content.querySelectorAll('.messageCheckbox') - .forEach(el => el.append(Element.fromHTML(``))); - } }); })(window.rl); diff --git a/plugins/avatars/images/services/amazon.com.png b/plugins/avatars/images/services/amazon.com.png index 4717c09c7..ec102e472 100644 Binary files a/plugins/avatars/images/services/amazon.com.png and b/plugins/avatars/images/services/amazon.com.png differ diff --git a/plugins/avatars/images/services/disneyplus.com.png b/plugins/avatars/images/services/disneyplus.com.png index 7f75ec823..3609fb3e7 100644 Binary files a/plugins/avatars/images/services/disneyplus.com.png and b/plugins/avatars/images/services/disneyplus.com.png differ diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 06f8496e9..21b2be697 100644 --- a/plugins/avatars/index.php +++ b/plugins/avatars/index.php @@ -10,12 +10,12 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin NAME = 'Avatars', AUTHOR = 'SnappyMail', URL = 'https://snappymail.eu/', - VERSION = '1.11', - RELEASE = '2023-02-23', - REQUIRED = '2.25.0', + VERSION = '1.20', + RELEASE = '2024-08-26', + REQUIRED = '2.33.0', CATEGORY = 'Contacts', LICENSE = 'MIT', - DESCRIPTION = 'Show graphic of sender in message and messages list (supports BIMI, Gravatar and identicon, Contacts is still TODO)'; + DESCRIPTION = 'Show graphic of sender in message and messages list (supports BIMI, Gravatar, favicon and identicon, Contacts is still TODO)'; public function Init() : void { @@ -28,16 +28,31 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addJs("{$identicon}.js"); } // https://github.com/the-djmaze/snappymail/issues/714 - if ($this->Config()->Get('plugin', 'service', true) || !$this->Config()->Get('plugin', 'delay', true)) { + if ($this->Config()->Get('plugin', 'service', true) +// || !$this->Config()->Get('plugin', 'delay', true) + || $this->Config()->Get('plugin', 'gravatar', false) + || $this->Config()->Get('plugin', 'bimi', false) + || $this->Config()->Get('plugin', 'favicon', false) + ) { $this->addHook('json.after-message', 'JsonMessage'); $this->addHook('json.after-messagelist', 'JsonMessageList'); } + // https://www.ietf.org/archive/id/draft-brand-indicators-for-message-identification-04.html#bimi-selector + if ($this->Config()->Get('plugin', 'bimi', false)) { + $this->addHook('imap.message-headers', 'ImapMessageHeaders'); + } + } + + public function ImapMessageHeaders(array &$aHeaders) + { + // \MailSo\Mime\Enumerations\Header::BIMI_SELECTOR + $aHeaders[] = 'BIMI-Selector'; } public function JsonMessage(array &$aResponse) { if ($icon = $this->JsonAvatar($aResponse['Result'])) { - $aResponse['Result']['Avatar'] = $icon; + $aResponse['Result']['avatar'] = $icon; } } @@ -46,7 +61,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin if (!empty($aResponse['Result']['@Collection'])) { foreach ($aResponse['Result']['@Collection'] as &$message) { if ($icon = $this->JsonAvatar($message)) { - $message['Avatar'] = $icon; + $message['avatar'] = $icon; } } } @@ -59,14 +74,10 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $mFrom = $mFrom->jsonSerialize(); } if (\is_array($mFrom)) { - if ('pass' == $mFrom['dkimStatus'] && $this->Config()->Get('plugin', 'service', true)) { - // 'data:image/png;base64,[a-zA-Z0-9+/=]' - return static::getServiceIcon($mFrom['email']); - } - if (!$this->Config()->Get('plugin', 'delay', true) - && ($this->Config()->Get('plugin', 'gravatar', false) + if (/*!$this->Config()->Get('plugin', 'delay', true) + && */($this->Config()->Get('plugin', 'gravatar', false) || ($this->Config()->Get('plugin', 'bimi', false) && 'pass' == $mFrom['dkimStatus']) - || !$this->Config()->Get('plugin', 'service', true) + || ($this->Config()->Get('plugin', 'favicon', false) && 'pass' == $mFrom['dkimStatus']) ) ) try { // Base64Url @@ -74,6 +85,10 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin } catch (\Throwable $e) { \SnappyMail\Log::error('Crypt', $e->getMessage()); } + if ('pass' == $mFrom['dkimStatus'] && $this->Config()->Get('plugin', 'service', true)) { + // 'data:image/png;base64,[a-zA-Z0-9+/=]' + return static::getServiceIcon($mFrom['email']); + } } return null; } @@ -84,8 +99,9 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin public function DoAvatar() : array { $bBimi = !empty($this->jsonParam('bimi')); + $sBimiSelector = $this->jsonParam('bimiSelector') ?: ''; $sEmail = $this->jsonParam('email'); - $aResult = $this->getAvatar($sEmail, !empty($bBimi)); + $aResult = $this->getAvatar($sEmail, $bBimi, $sBimiSelector); if ($aResult) { $aResult = [ 'type' => $aResult[0], @@ -100,10 +116,16 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin * Nextcloud Mail uses insecure unencrypted 'index.php/apps/mail/api/avatars/url/local%40example.com' */ // public function ServiceAvatar(...$aParts) - public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEmail) + public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEncryptedEmail) { - $sEmail = \SnappyMail\Crypt::DecryptUrlSafe($sEmail); - if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($sBimi)))) { + $maxAge = 86400; + $sEmail = \SnappyMail\Crypt::DecryptUrlSafe($sEncryptedEmail); + $aBimi = \explode('-', $sBimi, 2); + $sBimiSelector = isset($aBimi[1]) ? $aBimi[1] : 'default'; +// $sEmail && \MailSo\Base\Http::setETag("{$sBimiSelector}-{$sEncryptedEmail}"); + if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($aBimi[0]), $sBimiSelector))) { + \header("Cache-Control: max-age={$maxAge}, private"); + \header('Expires: '.\gmdate('D, j M Y H:i:s', $maxAge + \time()).' UTC'); \header('Content-Type: '.$aResult[0]); echo $aResult[1]; } else { @@ -122,12 +144,14 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin ->SetDefaultValue(true), \RainLoop\Plugins\Property::NewInstance('bimi')->SetLabel('BIMI') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) -// ->SetAllowedInJs(true) ->SetDefaultValue(false) ->SetDescription('https://bimigroup.org/ (DKIM header must be valid)'), + \RainLoop\Plugins\Property::NewInstance('favicon')->SetLabel('Favicon') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(false) + ->SetDescription('Fetch favicon from domain (DKIM header must be valid)'), \RainLoop\Plugins\Property::NewInstance('gravatar')->SetLabel('Gravatar') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) -// ->SetAllowedInJs(true) ->SetDefaultValue(false) ->SetDescription('https://wikipedia.org/wiki/Gravatar'), ]); @@ -135,7 +159,6 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin defined('RainLoop\\Enumerations\\PluginPropertyType::SELECT') ? \RainLoop\Plugins\Property::NewInstance('identicon')->SetLabel('Identicon') ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECT) -// ->SetAllowedInJs(true) ->SetDefaultValue([ ['id' => '', 'name' => 'Name characters else silhouette'], ['id' => 'identicon', 'name' => 'Name characters else squares'], @@ -144,7 +167,6 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin ->SetDescription('https://wikipedia.org/wiki/Identicon') : \RainLoop\Plugins\Property::NewInstance('identicon')->SetLabel('Identicon') ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) -// ->SetAllowedInJs(true) ->SetDefaultValue(['','identicon','jdenticon']) ->SetDescription('empty = default, identicon = squares, jdenticon = Triangles shape') , @@ -190,13 +212,13 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin return null; } - private function getAvatar(string $sEmail, bool $bBimi) : ?array + private function getAvatar(string $sEmail, bool $bBimi, string $sBimiSelector = '') : ?array { if (!\strpos($sEmail, '@')) { return null; } - $sAsciiEmail = \mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true)); + $sAsciiEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail)); $sEmailId = \sha1($sAsciiEmail); \MailSo\Base\Http::setETag($sEmailId); @@ -235,7 +257,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $aUrls = []; if ($this->Config()->Get('plugin', 'bimi', false)) { - $BIMI = $bBimi ? \SnappyMail\DNS::BIMI($sDomain) : null; + $BIMI = $bBimi ? \SnappyMail\DNS::BIMI($sDomain, $sBimiSelector) : null; if ($BIMI) { $aUrls[] = $BIMI; // $aResult = ['text/uri-list', $BIMI]; @@ -246,7 +268,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin } if ($this->Config()->Get('plugin', 'gravatar', false)) { - $aUrls[] = 'http://gravatar.com/avatar/'.\md5(\strtolower($sAsciiEmail)).'?s=80&d=404'; + $aUrls[] = 'https://gravatar.com/avatar/'.\md5(\strtolower($sAsciiEmail)).'?s=80&d=404'; } foreach ($aUrls as $sUrl) { @@ -277,11 +299,10 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin break; } } -/* - if (!$aResult) { - $aResult = static::getFavicon($sEmail, $sDomain); + + if (!$aResult && $this->Config()->Get('plugin', 'favicon', false)) { + $aResult = static::getFavicon($sDomain); } -*/ } return $aResult; @@ -292,16 +313,20 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $sDomain = \preg_replace('/^(.+\\.)?(paypal\\.[a-z][a-z])$/D', 'paypal.com', $sDomain); $sDomain = \preg_replace('/^facebookmail.com$/D', 'facebook.com', $sDomain); $sDomain = \preg_replace('/^dhlparcel.nl$/D', 'dhl.com', $sDomain); + $sDomain = \preg_replace('/^amazon.nl$/D', 'amazon.com', $sDomain); $sDomain = \preg_replace('/^.+\\.([^.]+\\.[^.]+)$/D', '$1', $sDomain); return $sDomain; } private static function cacheImage(string $sEmail, array $aResult) : void { - $sEmailId = \sha1(\mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true))); if (!\is_dir(\APP_PRIVATE_DATA . 'avatars')) { \mkdir(\APP_PRIVATE_DATA . 'avatars', 0700); } + $sEmailId = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail)); + if (\str_contains($sEmail, '@')) { + $sEmailId = \sha1($sEmailId); + } \file_put_contents( \APP_PRIVATE_DATA . 'avatars/' . $sEmailId . \SnappyMail\File\MimeType::toExtension($aResult[0]), $aResult[1] @@ -311,35 +336,63 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin private static function getCachedImage(string $sEmail) : ?array { - $sEmail = \mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true)); + $sEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail)); $aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmail}.*"); - if ($aFiles) { - \MailSo\Base\Http::setLastModified(\filemtime($aFiles[0])); - return [ - \mime_content_type($aFiles[0]), - \file_get_contents($aFiles[0]) - ]; + if (!$aFiles && \str_contains($sEmail, '@')) { + $sEmailId = \sha1($sEmail); + $aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmailId}.*"); + if (!$aFiles) { + $sDomain = \explode('@', $sEmail); + $sDomain = \array_pop($sDomain); + $aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sDomain}.*"); + } } - $sEmailId = \sha1($sEmail); - $aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmailId}.*"); if ($aFiles) { - \MailSo\Base\Http::setLastModified(\filemtime($aFiles[0])); return [ - \mime_content_type($aFiles[0]), + \SnappyMail\File\MimeType::fromFile($aFiles[0]), \file_get_contents($aFiles[0]) ]; } return null; } - private static function getFavicon(string $sEmail, string $sDomain) : ?array + private static function getFavicon(string $sDomain) : ?array { $aResult = static::getUrl('https://' . $sDomain . '/favicon.ico') ?: static::getUrl('https://' . static::serviceDomain($sDomain) . '/favicon.ico') - ?: static::getUrl('https://www.' . static::serviceDomain($sDomain) . '/favicon.ico'); - // Also detect + ?: static::getUrl('https://www.' . static::serviceDomain($sDomain) . '/favicon.ico') + ?: static::getUrl("https://www.google.com/s2/favicons?sz=48&domain_url={$sDomain}") + ?: static::getUrl("https://api.faviconkit.com/{$sDomain}/48") +// ?: static::getUrl("https://api.statvoo.com/favicon/{$sDomain}") + ; +/* + Also detect the following? + + + + + + + + + + + + + + + + + + + + + + + +*/ if ($aResult) { - static::cacheImage($sEmail, $aResult); + static::cacheImage($sDomain, $aResult); } return $aResult; } diff --git a/plugins/proxyauth-login-example/LICENSE b/plugins/backup/LICENSE similarity index 96% rename from plugins/proxyauth-login-example/LICENSE rename to plugins/backup/LICENSE index 4a4ca8d81..f709b02e2 100644 --- a/plugins/proxyauth-login-example/LICENSE +++ b/plugins/backup/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 RainLoop Team +Copyright (c) 2016 RainLoop Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/plugins/backup/index.php b/plugins/backup/index.php new file mode 100644 index 000000000..1dc65f475 --- /dev/null +++ b/plugins/backup/index.php @@ -0,0 +1,95 @@ +addJs('js/BackupAdminSettings.js', true); // add js file + $this->addJsonHook('JsonAdminBackupData'); + $this->addJsonHook('JsonAdminRestoreData'); + $this->addTemplate('templates/BackupAdminSettingsTab.html', true); + } + + public function JsonAdminBackupData() + { + if (!($this->Manager()->Actions() instanceof \RainLoop\ActionsAdmin) + || !$this->Manager()->Actions()->IsAdminLoggined() + ) { + return $this->jsonResponse(__FUNCTION__, false); + } + + \file_put_contents(APP_PRIVATE_DATA.'cache/CACHEDIR.TAG', 'Signature: 8a477f597d28d172789f06886806bc55'); + + $sFileName = APP_PRIVATE_DATA . \MailSo\Base\Utils::Sha1Rand(); + + if (true) { + $sType = 'application/zip'; + $sFileName .= '.zip'; + if (\class_exists('ZipArchive')) { +// $oArchive = new \ZipArchive(); +// $oArchive->open($sFileName, \ZIPARCHIVE::CREATE | \ZIPARCHIVE::OVERWRITE); +// $oArchive->setArchiveComment('SnappyMail/'.APP_VERSION); + } + $oArchive = new \SnappyMail\Stream\ZIP($sFileName); + } else { + $sType = 'application/x-gzip'; + $sFileName .= '.tgz'; + $oArchive = new \SnappyMail\Stream\TAR($sFileName); + } + +// $oArchive->addRecursive(APP_PRIVATE_DATA, '#/(cache.*)#'); + $oArchive->addRecursive(APP_PRIVATE_DATA.'configs', 'configs'); + $oArchive->addRecursive(APP_PRIVATE_DATA.'domains', 'domains'); + $oArchive->addRecursive(APP_PRIVATE_DATA.'plugins', 'plugins'); + $oArchive->addRecursive(APP_PRIVATE_DATA.'storage', 'storage'); + if (\is_readable(APP_PRIVATE_DATA.'AddressBook.sqlite')) { + $oArchive->addFile(APP_PRIVATE_DATA.'AddressBook.sqlite'); + } +// $oArchive->addFile(APP_DATA_FOLDER_PATH.'SALT.php'); + $oArchive->close(); + + $data = \base64_encode(\file_get_contents($sFileName)); + \unlink($sFileName); + + return $this->jsonResponse(__FUNCTION__, array( + 'name' => \basename($sFileName), + 'data' => "data:{$sType};base64,{$data}" + )); + } + + public function JsonAdminRestoreData() + { + if (!($this->Manager()->Actions() instanceof \RainLoop\ActionsAdmin) + || empty($_FILES['backup']) + || 'application/zip' !== $_FILES['backup']['type'] + || !\is_uploaded_file($_FILES['backup']['tmp_name']) + ) { + return $this->jsonResponse(__FUNCTION__, false); + } + + $result = false; + if (\class_exists('ZipArchive')) { + $oArchive = new \ZipArchive(); + $oArchive->open($_FILES['backup']['tmp_name'], \ZIPARCHIVE::CREATE); + $result = $oArchive->extractTo(APP_PRIVATE_DATA); + } else if (\class_exists('PharData')) { + $oArchive = new \PharData($sTmp, 0, null, \Phar::GZ); + $result = $oArchive->extractTo(APP_PRIVATE_DATA); + } + + return $this->jsonResponse(__FUNCTION__, $result); + } + +} diff --git a/plugins/backup/js/BackupAdminSettings.js b/plugins/backup/js/BackupAdminSettings.js new file mode 100644 index 000000000..221b5d1f0 --- /dev/null +++ b/plugins/backup/js/BackupAdminSettings.js @@ -0,0 +1,47 @@ + +(rl => { if (rl) { + + class BackupAdminSettings + { + constructor() + { + this.loading = ko.observable(false); + } + + backup() + { + this.loading(true); + rl.pluginRemoteRequest((iError, oData) => { + + this.loading(false); + + if (iError) { + console.error({ + iError: iError, + oData: oData + }); + } else { + var link = document.createElement("a"); + link.download = oData.Result.name; + link.href = oData.Result.data; + link.textContent = oData.Result.name; + this.viewModelDom.append(link); + link.click(); + link.remove(); + } + + }, 'JsonAdminBackupData'); + } + + submitForm(form) { + form.reportValidity() + && rl.pluginRemoteRequest((iError, oData) => { + console.dir(oData); + }, 'JsonAdminRestoreData', new FormData(form)); + } + } + + rl.addSettingsViewModelForAdmin(BackupAdminSettings, 'BackupAdminSettingsTab', + 'Backup and Restore', 'Backup'); + +}})(window.rl); diff --git a/plugins/black-list/index.php b/plugins/black-list/index.php index 7f692f0e4..dea7e90ed 100644 --- a/plugins/black-list/index.php +++ b/plugins/black-list/index.php @@ -4,35 +4,31 @@ class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin { const NAME = 'Blacklist', - VERSION = '2.1', - RELEASE = '2021-04-21', + VERSION = '2.2', + RELEASE = '2024-03-04', REQUIRED = '2.5.0', CATEGORY = 'Login', DESCRIPTION = 'Simple blacklist extension (with wildcard and exceptions functionality).'; public function Init() : void { - $this->addHook('login.credentials', 'FilterLoginCredentials'); + $this->addHook('login.credentials.step-1', 'FilterLoginCredentials'); } /** - * @param string $sEmail - * @param string $sLogin - * @param string $sPassword - * * @throws \RainLoop\Exceptions\ClientException */ - public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword) + public function FilterLoginCredentials(string &$sEmail) { $sBlackList = \trim($this->Config()->Get('plugin', 'black_list', '')); - if (0 < \strlen($sBlackList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sBlackList)) - { + if (\strlen($sBlackList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sBlackList)) { $sExceptions = \trim($this->Config()->Get('plugin', 'exceptions', '')); - if (0 === \strlen($sExceptions) || !\RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sExceptions)) - { + if (!\strlen($sExceptions) || !\RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sExceptions)) { throw new \RainLoop\Exceptions\ClientException( - $this->Config()->Get('plugin', 'auth_error', true) ? - \RainLoop\Notifications::AuthError : \RainLoop\Notifications::AccountNotAllowed); + $this->Config()->Get('plugin', 'auth_error', false) + ? \RainLoop\Notifications::AuthError + : \RainLoop\Notifications::AccountNotAllowed + ); } } } @@ -46,7 +42,7 @@ class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin \RainLoop\Plugins\Property::NewInstance('auth_error')->SetLabel('Auth Error') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetDescription('Throw an authentication error instead of an access error.') - ->SetDefaultValue(true), + ->SetDefaultValue(false), \RainLoop\Plugins\Property::NewInstance('black_list')->SetLabel('Black List') ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) ->SetDescription('Emails black list, space as delimiter, wildcard supported.') diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/APCU.php b/plugins/cache-apcu/APCU.php similarity index 73% rename from snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/APCU.php rename to plugins/cache-apcu/APCU.php index 090aba857..8ca581ca0 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/APCU.php +++ b/plugins/cache-apcu/APCU.php @@ -20,11 +20,12 @@ class APCU implements \MailSo\Cache\DriverInterface { private string $sKeyPrefix; - function __construct(string $sKeyPrefix = '') + public function setPrefix(string $sKeyPrefix) : void { + $sKeyPrefix = \rtrim(\trim($sKeyPrefix), '\\/'); $this->sKeyPrefix = empty($sKeyPrefix) ? $sKeyPrefix - : \preg_replace('/[^a-zA-Z0-9_]/', '_', \rtrim(\trim($sKeyPrefix), '\\/')).'/'; + : \preg_replace('/[^a-zA-Z0-9_]/', '_', $sKeyPrefix).'/'; } public function Set(string $sKey, string $sValue) : bool @@ -32,10 +33,15 @@ class APCU implements \MailSo\Cache\DriverInterface return \apcu_store($this->generateCachedKey($sKey), (string) $sValue); } - public function Get(string $sKey) : string + public function Exists(string $sKey) : bool + { + return \apcu_exists($this->generateCachedKey($sKey)); + } + + public function Get(string $sKey) : ?string { $sValue = \apcu_fetch($this->generateCachedKey($sKey)); - return \is_string($sValue) ? $sValue : ''; + return \is_string($sValue) ? $sValue : null; } public function Delete(string $sKey) : void diff --git a/plugins/cache-apcu/index.php b/plugins/cache-apcu/index.php new file mode 100644 index 000000000..2e17413a4 --- /dev/null +++ b/plugins/cache-apcu/index.php @@ -0,0 +1,34 @@ +addHook('main.fabrica', 'MainFabrica'); + } + } + + public function Supported() : string + { + return \MailSo\Base\Utils::FunctionsCallable(array('apcu_store', 'apcu_fetch', 'apcu_delete', 'apcu_clear_cache')) + ? '' + : 'PHP APCu not installed'; + } + + public function MainFabrica($sName, &$mResult) + { + if ('cache' == $sName) { + require_once __DIR__ . '/APCU.php'; + $mResult = new \MailSo\Cache\Drivers\APCU; + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Memcache.php b/plugins/cache-memcache/Memcache.php similarity index 76% rename from snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Memcache.php rename to plugins/cache-memcache/Memcache.php index b5e2a5252..f967ad687 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Memcache.php +++ b/plugins/cache-memcache/Memcache.php @@ -27,7 +27,7 @@ class Memcache implements \MailSo\Cache\DriverInterface private string $sKeyPrefix; - function __construct(string $sHost = '127.0.0.1', int $iPort = 11211, int $iExpire = 43200, string $sKeyPrefix = '') + function __construct(string $sHost = '127.0.0.1', int $iPort = 11211, int $iExpire = 43200) { $this->iExpire = 0 < $iExpire ? $iExpire : 43200; @@ -35,10 +35,14 @@ class Memcache implements \MailSo\Cache\DriverInterface if (!$this->oMem->addServer($sHost, \strpos($sHost, ':/') ? 0 : $iPort)) { $this->oMem = null; } + } + public function setPrefix(string $sKeyPrefix) : void + { + $sKeyPrefix = \rtrim(\trim($sKeyPrefix), '\\/'); $this->sKeyPrefix = empty($sKeyPrefix) ? $sKeyPrefix - : \preg_replace('/[^a-zA-Z0-9_]/', '_', \rtrim(\trim($this->sKeyPrefix), '\\/')) . '/'; + : \preg_replace('/[^a-zA-Z0-9_]/', '_', $sKeyPrefix).'/'; } public function Set(string $sKey, string $sValue) : bool @@ -46,10 +50,15 @@ class Memcache implements \MailSo\Cache\DriverInterface return $this->oMem ? $this->oMem->set($this->generateCachedKey($sKey), $sValue, 0, $this->iExpire) : false; } - public function Get(string $sKey) : string + public function Exists(string $sKey) : bool { - $sValue = $this->oMem ? $this->oMem->get($this->generateCachedKey($sKey)) : ''; - return \is_string($sValue) ? $sValue : ''; + return $this->oMem && false !== $this->oMem->get($this->generateCachedKey($sKey)); + } + + public function Get(string $sKey) : ?string + { + $sValue = $this->oMem ? $this->oMem->get($this->generateCachedKey($sKey)) : null; + return \is_string($sValue) ? $sValue : null; } public function Delete(string $sKey) : void diff --git a/plugins/cache-memcache/index.php b/plugins/cache-memcache/index.php new file mode 100644 index 000000000..6a7354e13 --- /dev/null +++ b/plugins/cache-memcache/index.php @@ -0,0 +1,54 @@ +addHook('main.fabrica', 'MainFabrica'); + } + } + + public function Supported() : string + { + return (\class_exists('Memcache',false) || \class_exists('Memcached',false)) + ? '' + : 'PHP Memcache/Memcached not installed'; + } + + public function MainFabrica($sName, &$mResult) + { + if ('cache' == $sName) { + require_once __DIR__ . '/Memcache.php'; + $mResult = new \MailSo\Cache\Drivers\Memcache( + $this->Config()->Get('plugin', 'host', '127.0.0.1'), + (int) $this->Config()->Get('plugin', 'port', 11211) + ); + } + } + + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('host')->SetLabel('Host') + ->SetDescription('Hostname of the memcache server') + ->SetDefaultValue('127.0.0.1'), + \RainLoop\Plugins\Property::NewInstance('port')->SetLabel('Port') + ->SetDescription('Port of the memcache server') + ->SetDefaultValue(11211) +/* + ,\RainLoop\Plugins\Property::NewInstance('password')->SetLabel('Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) + ->SetDefaultValue('') +*/ + ); + } +} diff --git a/plugins/cache-redis/LICENSE b/plugins/cache-redis/LICENSE new file mode 100644 index 000000000..9a8cd865b --- /dev/null +++ b/plugins/cache-redis/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2009-2020 Daniele Alessandri (original work) +Copyright (c) 2021-2023 Till Krüss (modified work) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/cache-redis/Predis/Autoloader.php b/plugins/cache-redis/Predis/Autoloader.php new file mode 100644 index 000000000..054f7bbb2 --- /dev/null +++ b/plugins/cache-redis/Predis/Autoloader.php @@ -0,0 +1,64 @@ + + * @author Daniele Alessandri + * @codeCoverageIgnore + */ +class Autoloader +{ + private $directory; + private $prefix; + private $prefixLength; + + /** + * @param string $baseDirectory Base directory where the source files are located. + */ + public function __construct($baseDirectory = __DIR__) + { + $this->directory = $baseDirectory; + $this->prefix = __NAMESPACE__ . '\\'; + $this->prefixLength = strlen($this->prefix); + } + + /** + * Registers the autoloader class with the PHP SPL autoloader. + * + * @param bool $prepend Prepend the autoloader on the stack instead of appending it. + */ + public static function register($prepend = false) + { + spl_autoload_register([new self(), 'autoload'], true, $prepend); + } + + /** + * Loads a class from a file using its fully qualified name. + * + * @param string $className Fully qualified name of a class. + */ + public function autoload($className) + { + if (0 === strpos($className, $this->prefix)) { + $parts = explode('\\', substr($className, $this->prefixLength)); + $filepath = $this->directory . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts) . '.php'; + + if (is_file($filepath)) { + require $filepath; + } + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Client.php b/plugins/cache-redis/Predis/Client.php similarity index 51% rename from snappymail/v/0.0.0/app/libraries/Predis/Client.php rename to plugins/cache-redis/Predis/Client.php index 87596ec92..0b4511a52 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Client.php +++ b/plugins/cache-redis/Predis/Client.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,21 +12,35 @@ namespace Predis; +use ArrayIterator; +use InvalidArgumentException; +use IteratorAggregate; use Predis\Command\CommandInterface; use Predis\Command\RawCommand; +use Predis\Command\Redis\Container\ContainerFactory; +use Predis\Command\Redis\Container\ContainerInterface; use Predis\Command\ScriptCommand; use Predis\Configuration\Options; use Predis\Configuration\OptionsInterface; -use Predis\Connection\AggregateConnectionInterface; use Predis\Connection\ConnectionInterface; +use Predis\Connection\Parameters; use Predis\Connection\ParametersInterface; +use Predis\Connection\RelayConnection; use Predis\Monitor\Consumer as MonitorConsumer; +use Predis\Pipeline\Atomic; +use Predis\Pipeline\FireAndForget; use Predis\Pipeline\Pipeline; +use Predis\Pipeline\RelayAtomic; +use Predis\Pipeline\RelayPipeline; use Predis\PubSub\Consumer as PubSubConsumer; +use Predis\PubSub\RelayConsumer as RelayPubSubConsumer; use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\ResponseInterface; use Predis\Response\ServerException; use Predis\Transaction\MultiExec as MultiExecTransaction; +use ReturnTypeWillChange; +use RuntimeException; +use Traversable; /** * Client class used for connecting and executing commands on Redis. @@ -34,17 +49,20 @@ use Predis\Transaction\MultiExec as MultiExecTransaction; * abstractions are built. Internally it aggregates various other classes each * one with its own responsibility and scope. * - * {@inheritdoc} - * - * @author Daniele Alessandri + * @template-implements \IteratorAggregate */ -class Client implements ClientInterface +class Client implements ClientInterface, IteratorAggregate { - const VERSION = '1.0.3'; + public const VERSION = '2.2.2'; - protected $connection; - protected $options; - private $profile; + /** @var OptionsInterface */ + private $options; + + /** @var ConnectionInterface */ + private $connection; + + /** @var Command\FactoryInterface */ + private $commands; /** * @param mixed $parameters Connection parameters for one or more servers. @@ -52,126 +70,99 @@ class Client implements ClientInterface */ public function __construct($parameters = null, $options = null) { - $this->options = $this->createOptions($options ?: array()); - $this->connection = $this->createConnection($parameters ?: array()); - $this->profile = $this->options->profile; + $this->options = static::createOptions($options ?? new Options()); + $this->connection = static::createConnection($this->options, $parameters ?? new Parameters()); + $this->commands = $this->options->commands; } /** - * Creates a new instance of Predis\Configuration\Options from different - * types of arguments or simply returns the passed argument if it is an - * instance of Predis\Configuration\OptionsInterface. + * Creates a new set of client options for the client. * - * @param mixed $options Client options. - * - * @throws \InvalidArgumentException + * @param array|OptionsInterface $options Set of client options * * @return OptionsInterface + * @throws InvalidArgumentException */ - protected function createOptions($options) + protected static function createOptions($options) { if (is_array($options)) { return new Options($options); - } - - if ($options instanceof OptionsInterface) { + } elseif ($options instanceof OptionsInterface) { return $options; + } else { + throw new InvalidArgumentException('Invalid type for client options'); } - - throw new \InvalidArgumentException('Invalid type for client options.'); } /** - * Creates single or aggregate connections from different types of arguments - * (string, array) or returns the passed argument if it is an instance of a - * class implementing Predis\Connection\ConnectionInterface. + * Creates single or aggregate connections from supplied arguments. * - * Accepted types for connection parameters are: + * This method accepts the following types to create a connection instance: * - * - Instance of Predis\Connection\ConnectionInterface. - * - Instance of Predis\Connection\ParametersInterface. - * - Array - * - String - * - Callable + * - Array (dictionary: single connection, indexed: aggregate connections) + * - String (URI for a single connection) + * - Callable (connection initializer callback) + * - Instance of Predis\Connection\ParametersInterface (used as-is) + * - Instance of Predis\Connection\ConnectionInterface (returned as-is) * - * @param mixed $parameters Connection parameters or connection instance. + * When a callable is passed, it receives the original set of client options + * and must return an instance of Predis\Connection\ConnectionInterface. * - * @throws \InvalidArgumentException + * Connections are created using the connection factory (in case of single + * connections) or a specialized aggregate connection initializer (in case + * of cluster and replication) retrieved from the supplied client options. + * + * @param OptionsInterface $options Client options container + * @param mixed $parameters Connection parameters * * @return ConnectionInterface + * @throws InvalidArgumentException */ - protected function createConnection($parameters) + protected static function createConnection(OptionsInterface $options, $parameters) { if ($parameters instanceof ConnectionInterface) { return $parameters; } if ($parameters instanceof ParametersInterface || is_string($parameters)) { - return $this->options->connections->create($parameters); + return $options->connections->create($parameters); } if (is_array($parameters)) { if (!isset($parameters[0])) { - return $this->options->connections->create($parameters); - } - - $options = $this->options; - - if ($options->defined('aggregate')) { - $initializer = $this->getConnectionInitializerWrapper($options->aggregate); - $connection = $initializer($parameters, $options); + return $options->connections->create($parameters); + } elseif ($options->defined('cluster') && $initializer = $options->cluster) { + return $initializer($parameters, true); + } elseif ($options->defined('replication') && $initializer = $options->replication) { + return $initializer($parameters, true); + } elseif ($options->defined('aggregate') && $initializer = $options->aggregate) { + return $initializer($parameters, false); } else { - if ($options->defined('replication') && $replication = $options->replication) { - $connection = $replication; - } else { - $connection = $options->cluster; - } - - $options->connections->aggregate($connection, $parameters); + throw new InvalidArgumentException( + 'Array of connection parameters requires `cluster`, `replication` or `aggregate` client option' + ); } - - return $connection; } if (is_callable($parameters)) { - $initializer = $this->getConnectionInitializerWrapper($parameters); - $connection = $initializer($this->options); + $connection = call_user_func($parameters, $options); + + if (!$connection instanceof ConnectionInterface) { + throw new InvalidArgumentException('Callable parameters must return a valid connection'); + } return $connection; } - throw new \InvalidArgumentException('Invalid type for connection parameters.'); - } - - /** - * Wraps a callable to make sure that its returned value represents a valid - * connection type. - * - * @param mixed $callable - * - * @return \Closure - */ - protected function getConnectionInitializerWrapper($callable) - { - return function () use ($callable) { - $connection = call_user_func_array($callable, func_get_args()); - - if (!$connection instanceof ConnectionInterface) { - throw new \UnexpectedValueException( - 'The callable connection initializer returned an invalid type.' - ); - } - - return $connection; - }; + throw new InvalidArgumentException('Invalid type for connection parameters'); } /** * {@inheritdoc} */ - public function getProfile() + public function getCommandFactory() { - return $this->profile; + return $this->commands; } /** @@ -183,23 +174,53 @@ class Client implements ClientInterface } /** - * Creates a new client instance for the specified connection ID or alias, - * only when working with an aggregate connection (cluster, replication). - * The new client instances uses the same options of the original one. + * Creates a new client using a specific underlying connection. * - * @param string $connectionID Identifier of a connection. + * This method allows to create a new client instance by picking a specific + * connection out of an aggregate one, with the same options of the original + * client instance. * - * @throws \InvalidArgumentException + * The specified selector defines which logic to use to look for a suitable + * connection by the specified value. Supported selectors are: * - * @return Client + * - `id` + * - `key` + * - `slot` + * - `command` + * - `alias` + * - `role` + * + * Internally the client relies on duck-typing and follows this convention: + * + * $selector string => getConnectionBy$selector($value) method + * + * This means that support for specific selectors may vary depending on the + * actual logic implemented by connection classes and there is no interface + * binding a connection class to implement any of these. + * + * @param string $selector Type of selector. + * @param mixed $value Value to be used by the selector. + * + * @return ClientInterface */ - public function getClientFor($connectionID) + public function getClientBy($selector, $value) { - if (!$connection = $this->getConnectionById($connectionID)) { - throw new \InvalidArgumentException("Invalid connection ID: $connectionID."); + $selector = strtolower($selector); + + if (!in_array($selector, ['id', 'key', 'slot', 'role', 'alias', 'command'])) { + throw new InvalidArgumentException("Invalid selector type: `$selector`"); } - return new static($connection, $this->options); + if (!method_exists($this->connection, $method = "getConnectionBy$selector")) { + $class = get_class($this->connection); + throw new InvalidArgumentException("Selecting connection by $selector is not supported by $class"); + } + + if (!$connection = $this->connection->$method($value)) { + throw new InvalidArgumentException("Cannot find a connection by $selector matching `$value`"); + } + + return new static($connection, $this->getOptions()); } /** @@ -248,24 +269,29 @@ class Client implements ClientInterface } /** - * Retrieves the specified connection from the aggregate connection when the - * client is in cluster or replication mode. + * Applies the configured serializer and compression to given value. * - * @param string $connectionID Index or alias of the single connection. - * - * @throws NotSupportedException - * - * @return Connection\NodeConnectionInterface + * @param mixed $value + * @return string */ - public function getConnectionById($connectionID) + public function pack($value) { - if (!$this->connection instanceof AggregateConnectionInterface) { - throw new NotSupportedException( - 'Retrieving connections by ID is supported only by aggregate connections.' - ); - } + return $this->connection instanceof RelayConnection + ? $this->connection->pack($value) + : $value; + } - return $this->connection->getConnectionById($connectionID); + /** + * Deserializes and decompresses to given value. + * + * @param mixed $value + * @return string + */ + public function unpack($value) + { + return $this->connection instanceof RelayConnection + ? $this->connection->unpack($value) + : $value; } /** @@ -273,7 +299,7 @@ class Client implements ClientInterface * applying any prefix to keys or throwing exceptions on Redis errors even * regardless of client options. * - * It is possibile to indentify Redis error responses from normal responses + * It is possible to identify Redis error responses from normal responses * using the second optional argument which is populated by reference. * * @param array $arguments Command arguments as defined by the command signature. @@ -284,9 +310,10 @@ class Client implements ClientInterface public function executeRaw(array $arguments, &$error = null) { $error = false; + $commandID = array_shift($arguments); $response = $this->connection->executeCommand( - new RawCommand($arguments) + new RawCommand($commandID, $arguments) ); if ($response instanceof ResponseInterface) { @@ -313,9 +340,37 @@ class Client implements ClientInterface /** * {@inheritdoc} */ - public function createCommand($commandID, $arguments = array()) + public function createCommand($commandID, $arguments = []) { - return $this->profile->createCommand($commandID, $arguments); + return $this->commands->create($commandID, $arguments); + } + + /** + * @param string $name + * @return ContainerInterface + */ + public function __get(string $name) + { + return ContainerFactory::create($this, $name); + } + + /** + * @param string $name + * @param mixed $value + * @return mixed + */ + public function __set(string $name, $value) + { + throw new RuntimeException('Not allowed'); + } + + /** + * @param string $name + * @return mixed + */ + public function __isset(string $name) + { + throw new RuntimeException('Not allowed'); } /** @@ -342,17 +397,13 @@ class Client implements ClientInterface * @param CommandInterface $command Redis command that generated the error. * @param ErrorResponseInterface $response Instance of the error response. * - * @throws ServerException - * * @return mixed + * @throws ServerException */ protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $response) { if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') { - $eval = $this->createCommand('EVAL'); - $eval->setRawArguments($command->getEvalArguments()); - - $response = $this->executeCommand($eval); + $response = $this->executeCommand($command->getEvalCommand()); if (!$response instanceof ResponseInterface) { $response = $command->parseResponse($response); @@ -370,7 +421,7 @@ class Client implements ClientInterface /** * Executes the specified initializer method on `$this` by adjusting the - * actual invokation depending on the arity (0, 1 or 2 arguments). This is + * actual invocation depending on the arity (0, 1 or 2 arguments). This is * simply an utility method to create Redis contexts instances since they * follow a common initialization path. * @@ -391,7 +442,7 @@ class Client implements ClientInterface : $this->$initializer(null, $argv[0]); case 2: - list($arg0, $arg1) = $argv; + [$arg0, $arg1] = $argv; return $this->$initializer($arg0, $arg1); @@ -404,11 +455,11 @@ class Client implements ClientInterface * Creates a new pipeline context and returns it, or returns the results of * a pipeline executed inside the optionally provided callable object. * - * @param mixed ... Array of options, a callable for execution, or both. + * @param mixed ...$arguments Array of options, a callable for execution, or both. * * @return Pipeline|array */ - public function pipeline(/* arguments */) + public function pipeline(...$arguments) { return $this->sharedContextFactory('createPipeline', func_get_args()); } @@ -416,19 +467,29 @@ class Client implements ClientInterface /** * Actual pipeline context initializer method. * - * @param array $options Options for the context. - * @param mixed $callable Optional callable used to execute the context. + * @param array|null $options Options for the context. + * @param mixed $callable Optional callable used to execute the context. * * @return Pipeline|array */ protected function createPipeline(array $options = null, $callable = null) { if (isset($options['atomic']) && $options['atomic']) { - $class = 'Predis\Pipeline\Atomic'; + $class = Atomic::class; } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) { - $class = 'Predis\Pipeline\FireAndForget'; + $class = FireAndForget::class; } else { - $class = 'Predis\Pipeline\Pipeline'; + $class = Pipeline::class; + } + + if ($this->connection instanceof RelayConnection) { + if (isset($options['atomic']) && $options['atomic']) { + $class = RelayAtomic::class; + } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) { + throw new NotSupportedException('The "relay" extension does not support fire-and-forget pipelines.'); + } else { + $class = RelayPipeline::class; + } } /* @@ -447,11 +508,11 @@ class Client implements ClientInterface * Creates a new transaction context and returns it, or returns the results * of a transaction executed inside the optionally provided callable object. * - * @param mixed ... Array of options, a callable for execution, or both. + * @param mixed ...$arguments Array of options, a callable for execution, or both. * * @return MultiExecTransaction|array */ - public function transaction(/* arguments */) + public function transaction(...$arguments) { return $this->sharedContextFactory('createTransaction', func_get_args()); } @@ -476,14 +537,14 @@ class Client implements ClientInterface } /** - * Creates a new publis/subscribe context and returns it, or starts its loop + * Creates a new publish/subscribe context and returns it, or starts its loop * inside the optionally provided callable object. * - * @param mixed ... Array of options, a callable for execution, or both. + * @param mixed ...$arguments Array of options, a callable for execution, or both. * * @return PubSubConsumer|null */ - public function pubSubLoop(/* arguments */) + public function pubSubLoop(...$arguments) { return $this->sharedContextFactory('createPubSub', func_get_args()); } @@ -498,7 +559,11 @@ class Client implements ClientInterface */ protected function createPubSub(array $options = null, $callable = null) { - $pubsub = new PubSubConsumer($this, $options); + if ($this->connection instanceof RelayConnection) { + $pubsub = new RelayPubSubConsumer($this, $options); + } else { + $pubsub = new PubSubConsumer($this, $options); + } if (!isset($callable)) { return $pubsub; @@ -509,6 +574,8 @@ class Client implements ClientInterface $pubsub->stop(); } } + + return null; } /** @@ -520,4 +587,26 @@ class Client implements ClientInterface { return new MonitorConsumer($this); } + + /** + * @return Traversable + */ + #[ReturnTypeWillChange] + public function getIterator() + { + $clients = []; + $connection = $this->getConnection(); + + if (!$connection instanceof Traversable) { + return new ArrayIterator([ + (string) $connection => new static($connection, $this->getOptions()), + ]); + } + + foreach ($connection as $node) { + $clients[(string) $node] = new static($node, $this->getOptions()); + } + + return new ArrayIterator($clients); + } } diff --git a/plugins/cache-redis/Predis/ClientConfiguration.php b/plugins/cache-redis/Predis/ClientConfiguration.php new file mode 100644 index 000000000..c70cd61cf --- /dev/null +++ b/plugins/cache-redis/Predis/ClientConfiguration.php @@ -0,0 +1,42 @@ + [ + ['name' => 'Json', 'commandPrefix' => 'JSON'], + ['name' => 'BloomFilter', 'commandPrefix' => 'BF'], + ['name' => 'CuckooFilter', 'commandPrefix' => 'CF'], + ['name' => 'CountMinSketch', 'commandPrefix' => 'CMS'], + ['name' => 'TDigest', 'commandPrefix' => 'TDIGEST'], + ['name' => 'TopK', 'commandPrefix' => 'TOPK'], + ['name' => 'Search', 'commandPrefix' => 'FT'], + ['name' => 'TimeSeries', 'commandPrefix' => 'TS'], + ], + ]; + + /** + * Returns available modules with configuration. + * + * @return array|string[][] + */ + public static function getModules(): array + { + return self::$config['modules']; + } +} diff --git a/plugins/cache-redis/Predis/ClientContextInterface.php b/plugins/cache-redis/Predis/ClientContextInterface.php new file mode 100644 index 000000000..e443ee5d1 --- /dev/null +++ b/plugins/cache-redis/Predis/ClientContextInterface.php @@ -0,0 +1,380 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,8 +14,6 @@ namespace Predis; /** * Exception class that identifies client-side errors. - * - * @author Daniele Alessandri */ class ClientException extends PredisException { diff --git a/plugins/cache-redis/Predis/ClientInterface.php b/plugins/cache-redis/Predis/ClientInterface.php new file mode 100644 index 000000000..2937c5804 --- /dev/null +++ b/plugins/cache-redis/Predis/ClientInterface.php @@ -0,0 +1,431 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,21 +12,17 @@ namespace Predis\Cluster; +use InvalidArgumentException; use Predis\Command\CommandInterface; use Predis\Command\ScriptCommand; /** * Common class implementing the logic needed to support clustering strategies. - * - * @author Daniele Alessandri */ abstract class ClusterStrategy implements StrategyInterface { protected $commands; - /** - * - */ public function __construct() { $this->commands = $this->getDefaultCommands(); @@ -38,12 +35,12 @@ abstract class ClusterStrategy implements StrategyInterface */ protected function getDefaultCommands() { - $getKeyFromFirstArgument = array($this, 'getKeyFromFirstArgument'); - $getKeyFromAllArguments = array($this, 'getKeyFromAllArguments'); + $getKeyFromFirstArgument = [$this, 'getKeyFromFirstArgument']; + $getKeyFromAllArguments = [$this, 'getKeyFromAllArguments']; - return array( + return [ /* commands operating on the key space */ - 'EXISTS' => $getKeyFromFirstArgument, + 'EXISTS' => $getKeyFromAllArguments, 'DEL' => $getKeyFromAllArguments, 'TYPE' => $getKeyFromFirstArgument, 'EXPIRE' => $getKeyFromFirstArgument, @@ -53,9 +50,10 @@ abstract class ClusterStrategy implements StrategyInterface 'PEXPIREAT' => $getKeyFromFirstArgument, 'TTL' => $getKeyFromFirstArgument, 'PTTL' => $getKeyFromFirstArgument, - 'SORT' => $getKeyFromFirstArgument, // TODO + 'SORT' => [$this, 'getKeyFromSortCommand'], 'DUMP' => $getKeyFromFirstArgument, 'RESTORE' => $getKeyFromFirstArgument, + 'FLUSHDB' => [$this, 'getFakeKey'], /* commands operating on string values */ 'APPEND' => $getKeyFromFirstArgument, @@ -72,14 +70,15 @@ abstract class ClusterStrategy implements StrategyInterface 'INCRBYFLOAT' => $getKeyFromFirstArgument, 'SETBIT' => $getKeyFromFirstArgument, 'SETEX' => $getKeyFromFirstArgument, - 'MSET' => array($this, 'getKeyFromInterleavedArguments'), - 'MSETNX' => array($this, 'getKeyFromInterleavedArguments'), + 'MSET' => [$this, 'getKeyFromInterleavedArguments'], + 'MSETNX' => [$this, 'getKeyFromInterleavedArguments'], 'SETNX' => $getKeyFromFirstArgument, 'SETRANGE' => $getKeyFromFirstArgument, 'STRLEN' => $getKeyFromFirstArgument, 'SUBSTR' => $getKeyFromFirstArgument, - 'BITOP' => array($this, 'getKeyFromBitOp'), + 'BITOP' => [$this, 'getKeyFromBitOp'], 'BITCOUNT' => $getKeyFromFirstArgument, + 'BITFIELD' => $getKeyFromFirstArgument, /* commands operating on lists */ 'LINSERT' => $getKeyFromFirstArgument, @@ -88,9 +87,9 @@ abstract class ClusterStrategy implements StrategyInterface 'LPOP' => $getKeyFromFirstArgument, 'RPOP' => $getKeyFromFirstArgument, 'RPOPLPUSH' => $getKeyFromAllArguments, - 'BLPOP' => array($this, 'getKeyFromBlockingListCommands'), - 'BRPOP' => array($this, 'getKeyFromBlockingListCommands'), - 'BRPOPLPUSH' => array($this, 'getKeyFromBlockingListCommands'), + 'BLPOP' => [$this, 'getKeyFromBlockingListCommands'], + 'BRPOP' => [$this, 'getKeyFromBlockingListCommands'], + 'BRPOPLPUSH' => [$this, 'getKeyFromBlockingListCommands'], 'LPUSH' => $getKeyFromFirstArgument, 'LPUSHX' => $getKeyFromFirstArgument, 'RPUSH' => $getKeyFromFirstArgument, @@ -121,7 +120,7 @@ abstract class ClusterStrategy implements StrategyInterface 'ZCARD' => $getKeyFromFirstArgument, 'ZCOUNT' => $getKeyFromFirstArgument, 'ZINCRBY' => $getKeyFromFirstArgument, - 'ZINTERSTORE' => array($this, 'getKeyFromZsetAggregationCommands'), + 'ZINTERSTORE' => [$this, 'getKeyFromZsetAggregationCommands'], 'ZRANGE' => $getKeyFromFirstArgument, 'ZRANGEBYSCORE' => $getKeyFromFirstArgument, 'ZRANK' => $getKeyFromFirstArgument, @@ -132,7 +131,7 @@ abstract class ClusterStrategy implements StrategyInterface 'ZREVRANGEBYSCORE' => $getKeyFromFirstArgument, 'ZREVRANK' => $getKeyFromFirstArgument, 'ZSCORE' => $getKeyFromFirstArgument, - 'ZUNIONSTORE' => array($this, 'getKeyFromZsetAggregationCommands'), + 'ZUNIONSTORE' => [$this, 'getKeyFromZsetAggregationCommands'], 'ZSCAN' => $getKeyFromFirstArgument, 'ZLEXCOUNT' => $getKeyFromFirstArgument, 'ZRANGEBYLEX' => $getKeyFromFirstArgument, @@ -162,9 +161,23 @@ abstract class ClusterStrategy implements StrategyInterface 'PFMERGE' => $getKeyFromAllArguments, /* scripting */ - 'EVAL' => array($this, 'getKeyFromScriptingCommands'), - 'EVALSHA' => array($this, 'getKeyFromScriptingCommands'), - ); + 'EVAL' => [$this, 'getKeyFromScriptingCommands'], + 'EVALSHA' => [$this, 'getKeyFromScriptingCommands'], + + /* server */ + 'INFO' => [$this, 'getFakeKey'], + + /* commands performing geospatial operations */ + 'GEOADD' => $getKeyFromFirstArgument, + 'GEOHASH' => $getKeyFromFirstArgument, + 'GEOPOS' => $getKeyFromFirstArgument, + 'GEODIST' => $getKeyFromFirstArgument, + 'GEORADIUS' => [$this, 'getKeyFromGeoradiusCommands'], + 'GEORADIUSBYMEMBER' => [$this, 'getKeyFromGeoradiusCommands'], + + /* cluster */ + 'CLUSTER' => [$this, 'getFakeKey'], + ]; } /** @@ -189,7 +202,7 @@ abstract class ClusterStrategy implements StrategyInterface * @param string $commandID Command ID. * @param mixed $callback A valid callable object, or NULL to unset the handler. * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function setCommandHandler($commandID, $callback = null) { @@ -202,7 +215,7 @@ abstract class ClusterStrategy implements StrategyInterface } if (!is_callable($callback)) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'The argument must be a callable object or NULL.' ); } @@ -210,6 +223,16 @@ abstract class ClusterStrategy implements StrategyInterface $this->commands[$commandID] = $callback; } + /** + * Get fake key for commands with no key argument. + * + * @return string + */ + protected function getFakeKey(): string + { + return 'key'; + } + /** * Extracts the key from the first argument of a command instance. * @@ -234,9 +257,11 @@ abstract class ClusterStrategy implements StrategyInterface { $arguments = $command->getArguments(); - if ($this->checkSameSlotForKeys($arguments)) { - return $arguments[0]; + if (!$this->checkSameSlotForKeys($arguments)) { + return null; } + + return $arguments[0]; } /** @@ -250,15 +275,48 @@ abstract class ClusterStrategy implements StrategyInterface protected function getKeyFromInterleavedArguments(CommandInterface $command) { $arguments = $command->getArguments(); - $keys = array(); + $keys = []; for ($i = 0; $i < count($arguments); $i += 2) { $keys[] = $arguments[$i]; } - if ($this->checkSameSlotForKeys($keys)) { - return $arguments[0]; + if (!$this->checkSameSlotForKeys($keys)) { + return null; } + + return $arguments[0]; + } + + /** + * Extracts the key from SORT command. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromSortCommand(CommandInterface $command) + { + $arguments = $command->getArguments(); + $firstKey = $arguments[0]; + + if (1 === $argc = count($arguments)) { + return $firstKey; + } + + $keys = [$firstKey]; + + for ($i = 1; $i < $argc; ++$i) { + if (strtoupper($arguments[$i]) === 'STORE') { + $keys[] = $arguments[++$i]; + } + } + + if (!$this->checkSameSlotForKeys($keys)) { + return null; + } + + return $firstKey; } /** @@ -272,9 +330,11 @@ abstract class ClusterStrategy implements StrategyInterface { $arguments = $command->getArguments(); - if ($this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) { - return $arguments[0]; + if (!$this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) { + return null; } + + return $arguments[0]; } /** @@ -288,9 +348,42 @@ abstract class ClusterStrategy implements StrategyInterface { $arguments = $command->getArguments(); - if ($this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) { - return $arguments[1]; + if (!$this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) { + return null; } + + return $arguments[1]; + } + + /** + * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromGeoradiusCommands(CommandInterface $command) + { + $arguments = $command->getArguments(); + $argc = count($arguments); + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; + + if ($argc > $startIndex) { + $keys = [$arguments[0]]; + + for ($i = $startIndex; $i < $argc; ++$i) { + $argument = strtoupper($arguments[$i]); + if ($argument === 'STORE' || $argument === 'STOREDIST') { + $keys[] = $arguments[++$i]; + } + } + + if (!$this->checkSameSlotForKeys($keys)) { + return null; + } + } + + return $arguments[0]; } /** @@ -303,11 +396,13 @@ abstract class ClusterStrategy implements StrategyInterface protected function getKeyFromZsetAggregationCommands(CommandInterface $command) { $arguments = $command->getArguments(); - $keys = array_merge(array($arguments[0]), array_slice($arguments, 2, $arguments[1])); + $keys = array_merge([$arguments[0]], array_slice($arguments, 2, $arguments[1])); - if ($this->checkSameSlotForKeys($keys)) { - return $arguments[0]; + if (!$this->checkSameSlotForKeys($keys)) { + return null; } + + return $arguments[0]; } /** @@ -319,15 +414,15 @@ abstract class ClusterStrategy implements StrategyInterface */ protected function getKeyFromScriptingCommands(CommandInterface $command) { - if ($command instanceof ScriptCommand) { - $keys = $command->getKeys(); - } else { - $keys = array_slice($args = $command->getArguments(), 2, $args[1]); + $keys = $command instanceof ScriptCommand + ? $command->getKeys() + : array_slice($args = $command->getArguments(), 2, $args[1]); + + if (!$keys || !$this->checkSameSlotForKeys($keys)) { + return null; } - if ($keys && $this->checkSameSlotForKeys($keys)) { - return $keys[0]; - } + return $keys[0]; } /** diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/DistributorInterface.php b/plugins/cache-redis/Predis/Cluster/Distributor/DistributorInterface.php similarity index 94% rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/DistributorInterface.php rename to plugins/cache-redis/Predis/Cluster/Distributor/DistributorInterface.php index 831f52c52..593d9bb3c 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/DistributorInterface.php +++ b/plugins/cache-redis/Predis/Cluster/Distributor/DistributorInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,8 +17,6 @@ use Predis\Cluster\Hash\HashGeneratorInterface; /** * A distributor implements the logic to automatically distribute keys among * several nodes for client-side sharding. - * - * @author Daniele Alessandri */ interface DistributorInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/EmptyRingException.php b/plugins/cache-redis/Predis/Cluster/Distributor/EmptyRingException.php similarity index 66% rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/EmptyRingException.php rename to plugins/cache-redis/Predis/Cluster/Distributor/EmptyRingException.php index 039f2f2e8..68172066e 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/EmptyRingException.php +++ b/plugins/cache-redis/Predis/Cluster/Distributor/EmptyRingException.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,11 +12,11 @@ namespace Predis\Cluster\Distributor; +use Exception; + /** * Exception class that identifies empty rings. - * - * @author Daniele Alessandri */ -class EmptyRingException extends \Exception +class EmptyRingException extends Exception { } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/HashRing.php b/plugins/cache-redis/Predis/Cluster/Distributor/HashRing.php similarity index 94% rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/HashRing.php rename to plugins/cache-redis/Predis/Cluster/Distributor/HashRing.php index db864d912..03e75106f 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/HashRing.php +++ b/plugins/cache-redis/Predis/Cluster/Distributor/HashRing.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -17,21 +18,19 @@ use Predis\Cluster\Hash\HashGeneratorInterface; * This class implements an hashring-based distributor that uses the same * algorithm of memcache to distribute keys in a cluster using client-side * sharding. - * - * @author Daniele Alessandri * @author Lorenzo Castelli */ class HashRing implements DistributorInterface, HashGeneratorInterface { - const DEFAULT_REPLICAS = 128; - const DEFAULT_WEIGHT = 100; + public const DEFAULT_REPLICAS = 128; + public const DEFAULT_WEIGHT = 100; private $ring; private $ringKeys; private $ringKeysCount; private $replicas; private $nodeHashCallback; - private $nodes = array(); + private $nodes = []; /** * @param int $replicas Number of replicas in the ring. @@ -53,10 +52,10 @@ class HashRing implements DistributorInterface, HashGeneratorInterface { // In case of collisions in the hashes of the nodes, the node added // last wins, thus the order in which nodes are added is significant. - $this->nodes[] = array( + $this->nodes[] = [ 'object' => $node, 'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT, - ); + ]; $this->reset(); } @@ -131,7 +130,7 @@ class HashRing implements DistributorInterface, HashGeneratorInterface throw new EmptyRingException('Cannot initialize an empty hashring.'); } - $this->ring = array(); + $this->ring = []; $totalWeight = $this->computeTotalWeight(); $nodesCount = count($this->nodes); @@ -161,7 +160,7 @@ class HashRing implements DistributorInterface, HashGeneratorInterface $replicas = (int) round($weightRatio * $totalNodes * $replicas); for ($i = 0; $i < $replicas; ++$i) { - $key = crc32("$nodeHash:$i"); + $key = $this->hash("$nodeHash:$i"); $ring[$key] = $nodeObject; } } @@ -239,9 +238,8 @@ class HashRing implements DistributorInterface, HashGeneratorInterface public function get($value) { $hash = $this->hash($value); - $node = $this->getByHash($hash); - return $node; + return $this->getByHash($hash); } /** diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/KetamaRing.php b/plugins/cache-redis/Predis/Cluster/Distributor/KetamaRing.php similarity index 92% rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/KetamaRing.php rename to plugins/cache-redis/Predis/Cluster/Distributor/KetamaRing.php index dc77f320f..af3b88455 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Distributor/KetamaRing.php +++ b/plugins/cache-redis/Predis/Cluster/Distributor/KetamaRing.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,13 +16,11 @@ namespace Predis\Cluster\Distributor; * This class implements an hashring-based distributor that uses the same * algorithm of libketama to distribute keys in a cluster using client-side * sharding. - * - * @author Daniele Alessandri * @author Lorenzo Castelli */ class KetamaRing extends HashRing { - const DEFAULT_REPLICAS = 160; + public const DEFAULT_REPLICAS = 160; /** * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes. diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/CRC16.php b/plugins/cache-redis/Predis/Cluster/Hash/CRC16.php similarity index 95% rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/CRC16.php rename to plugins/cache-redis/Predis/Cluster/Hash/CRC16.php index 3add0cef2..4b21d5d21 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/CRC16.php +++ b/plugins/cache-redis/Predis/Cluster/Hash/CRC16.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,12 +14,10 @@ namespace Predis\Cluster\Hash; /** * Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster. - * - * @author Daniele Alessandri */ class CRC16 implements HashGeneratorInterface { - private static $CCITT_16 = array( + private static $CCITT_16 = [ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, @@ -51,7 +50,7 @@ class CRC16 implements HashGeneratorInterface 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0, - ); + ]; /** * {@inheritdoc} @@ -61,6 +60,8 @@ class CRC16 implements HashGeneratorInterface // CRC-CCITT-16 algorithm $crc = 0; $CCITT_16 = self::$CCITT_16; + + $value = (string) $value; $strlen = strlen($value); for ($i = 0; $i < $strlen; ++$i) { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/HashGeneratorInterface.php b/plugins/cache-redis/Predis/Cluster/Hash/HashGeneratorInterface.php similarity index 84% rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/HashGeneratorInterface.php rename to plugins/cache-redis/Predis/Cluster/Hash/HashGeneratorInterface.php index 271b9e720..c835c0e66 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/Hash/HashGeneratorInterface.php +++ b/plugins/cache-redis/Predis/Cluster/Hash/HashGeneratorInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,8 +15,6 @@ namespace Predis\Cluster\Hash; /** * An hash generator implements the logic used to calculate the hash of a key to * distribute operations among Redis nodes. - * - * @author Daniele Alessandri */ interface HashGeneratorInterface { diff --git a/plugins/cache-redis/Predis/Cluster/Hash/PhpiredisCRC16.php b/plugins/cache-redis/Predis/Cluster/Hash/PhpiredisCRC16.php new file mode 100644 index 000000000..04f58a0ec --- /dev/null +++ b/plugins/cache-redis/Predis/Cluster/Hash/PhpiredisCRC16.php @@ -0,0 +1,42 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,8 +17,6 @@ use Predis\Cluster\Distributor\HashRing; /** * Default cluster strategy used by Predis to handle client-side sharding. - * - * @author Daniele Alessandri */ class PredisStrategy extends ClusterStrategy { @@ -40,9 +39,8 @@ class PredisStrategy extends ClusterStrategy { $key = $this->extractKeyTag($key); $hash = $this->distributor->hash($key); - $slot = $this->distributor->getSlot($hash); - return $slot; + return $this->distributor->getSlot($hash); } /** diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/RedisStrategy.php b/plugins/cache-redis/Predis/Cluster/RedisStrategy.php similarity index 76% rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/RedisStrategy.php rename to plugins/cache-redis/Predis/Cluster/RedisStrategy.php index df0bdb49b..8ae5c0f5e 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/RedisStrategy.php +++ b/plugins/cache-redis/Predis/Cluster/RedisStrategy.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -18,8 +19,6 @@ use Predis\NotSupportedException; /** * Default class used by Predis to calculate hashes out of keys of * commands supported by redis-cluster. - * - * @author Daniele Alessandri */ class RedisStrategy extends ClusterStrategy { @@ -41,9 +40,8 @@ class RedisStrategy extends ClusterStrategy public function getSlotByKey($key) { $key = $this->extractKeyTag($key); - $slot = $this->hashGenerator->hash($key) & 0x3FFF; - return $slot; + return $this->hashGenerator->hash($key) & 0x3FFF; } /** @@ -51,8 +49,7 @@ class RedisStrategy extends ClusterStrategy */ public function getDistributor() { - throw new NotSupportedException( - 'This cluster strategy does not provide an external distributor' - ); + $class = get_class($this); + throw new NotSupportedException("$class does not provide an external distributor"); } } diff --git a/plugins/cache-redis/Predis/Cluster/SlotMap.php b/plugins/cache-redis/Predis/Cluster/SlotMap.php new file mode 100644 index 000000000..1af63077a --- /dev/null +++ b/plugins/cache-redis/Predis/Cluster/SlotMap.php @@ -0,0 +1,209 @@ += 0x0000 && $slot <= 0x3FFF; + } + + /** + * Checks if the given slot range is valid. + * + * @param int $first Initial slot of the range. + * @param int $last Last slot of the range. + * + * @return bool + */ + public static function isValidRange($first, $last) + { + return $first >= 0x0000 && $first <= 0x3FFF && $last >= 0x0000 && $last <= 0x3FFF && $first <= $last; + } + + /** + * Resets the slot map. + */ + public function reset() + { + $this->slots = []; + } + + /** + * Checks if the slot map is empty. + * + * @return bool + */ + public function isEmpty() + { + return empty($this->slots); + } + + /** + * Returns the current slot map as a dictionary of $slot => $node. + * + * The order of the slots in the dictionary is not guaranteed. + * + * @return array + */ + public function toArray() + { + return $this->slots; + } + + /** + * Returns the list of unique nodes in the slot map. + * + * @return array + */ + public function getNodes() + { + return array_keys(array_flip($this->slots)); + } + + /** + * Assigns the specified slot range to a node. + * + * @param int $first Initial slot of the range. + * @param int $last Last slot of the range. + * @param NodeConnectionInterface|string $connection ID or connection instance. + * + * @throws OutOfBoundsException + */ + public function setSlots($first, $last, $connection) + { + if (!static::isValidRange($first, $last)) { + throw new OutOfBoundsException("Invalid slot range $first-$last for `$connection`"); + } + + $this->slots += array_fill($first, $last - $first + 1, (string) $connection); + } + + /** + * Returns the specified slot range. + * + * @param int $first Initial slot of the range. + * @param int $last Last slot of the range. + * + * @return array + */ + public function getSlots($first, $last) + { + if (!static::isValidRange($first, $last)) { + throw new OutOfBoundsException("Invalid slot range $first-$last"); + } + + return array_intersect_key($this->slots, array_fill($first, $last - $first + 1, null)); + } + + /** + * Checks if the specified slot is assigned. + * + * @param int $slot Slot index. + * + * @return bool + */ + #[ReturnTypeWillChange] + public function offsetExists($slot) + { + return isset($this->slots[$slot]); + } + + /** + * Returns the node assigned to the specified slot. + * + * @param int $slot Slot index. + * + * @return string|null + */ + #[ReturnTypeWillChange] + public function offsetGet($slot) + { + return $this->slots[$slot] ?? null; + } + + /** + * Assigns the specified slot to a node. + * + * @param int $slot Slot index. + * @param NodeConnectionInterface|string $connection ID or connection instance. + * + * @return void + */ + #[ReturnTypeWillChange] + public function offsetSet($slot, $connection) + { + if (!static::isValid($slot)) { + throw new OutOfBoundsException("Invalid slot $slot for `$connection`"); + } + + $this->slots[(int) $slot] = (string) $connection; + } + + /** + * Returns the node assigned to the specified slot. + * + * @param int $slot Slot index. + * + * @return void + */ + #[ReturnTypeWillChange] + public function offsetUnset($slot) + { + unset($this->slots[$slot]); + } + + /** + * Returns the current number of assigned slots. + * + * @return int + */ + #[ReturnTypeWillChange] + public function count() + { + return count($this->slots); + } + + /** + * Returns an iterator over the slot map. + * + * @return Traversable + */ + #[ReturnTypeWillChange] + public function getIterator() + { + return new ArrayIterator($this->slots); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/StrategyInterface.php b/plugins/cache-redis/Predis/Cluster/StrategyInterface.php similarity index 89% rename from snappymail/v/0.0.0/app/libraries/Predis/Cluster/StrategyInterface.php rename to plugins/cache-redis/Predis/Cluster/StrategyInterface.php index cdf7d09fa..83801ae6f 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Cluster/StrategyInterface.php +++ b/plugins/cache-redis/Predis/Cluster/StrategyInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -19,8 +20,6 @@ use Predis\Command\CommandInterface; * keys extracted from supported commands. * * This is mostly useful to support clustering via client-side sharding. - * - * @author Daniele Alessandri */ interface StrategyInterface { @@ -30,7 +29,7 @@ interface StrategyInterface * * @param CommandInterface $command Command instance. * - * @return int + * @return int|null */ public function getSlot(CommandInterface $command); @@ -40,7 +39,7 @@ interface StrategyInterface * * @param string $key Key string. * - * @return int + * @return int|null */ public function getSlotByKey($key); diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/CursorBasedIterator.php b/plugins/cache-redis/Predis/Collection/Iterator/CursorBasedIterator.php similarity index 83% rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/CursorBasedIterator.php rename to plugins/cache-redis/Predis/Collection/Iterator/CursorBasedIterator.php index 922883f05..946bbc3a0 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/CursorBasedIterator.php +++ b/plugins/cache-redis/Predis/Collection/Iterator/CursorBasedIterator.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,8 +12,10 @@ namespace Predis\Collection\Iterator; +use Iterator; use Predis\ClientInterface; use Predis\NotSupportedException; +use ReturnTypeWillChange; /** * Provides the base implementation for a fully-rewindable PHP iterator that can @@ -24,10 +27,8 @@ use Predis\NotSupportedException; * can change several times during the iteration process. * * @see http://redis.io/commands/scan - * - * @author Daniele Alessandri */ -abstract class CursorBasedIterator implements \Iterator +abstract class CursorBasedIterator implements Iterator { protected $client; protected $match; @@ -65,8 +66,8 @@ abstract class CursorBasedIterator implements \Iterator */ protected function requiredCommand(ClientInterface $client, $commandID) { - if (!$client->getProfile()->supportsCommand($commandID)) { - throw new NotSupportedException("The current profile does not support '$commandID'."); + if (!$client->getCommandFactory()->supports($commandID)) { + throw new NotSupportedException("'$commandID' is not supported by the current command factory."); } } @@ -77,7 +78,7 @@ abstract class CursorBasedIterator implements \Iterator { $this->valid = true; $this->fetchmore = true; - $this->elements = array(); + $this->elements = []; $this->cursor = 0; $this->position = -1; $this->current = null; @@ -90,9 +91,9 @@ abstract class CursorBasedIterator implements \Iterator */ protected function getScanOptions() { - $options = array(); + $options = []; - if (strlen($this->match) > 0) { + if (strlen(strval($this->match)) > 0) { $options['MATCH'] = $this->match; } @@ -117,7 +118,7 @@ abstract class CursorBasedIterator implements \Iterator */ protected function fetch() { - list($cursor, $elements) = $this->executeCommand(); + [$cursor, $elements] = $this->executeCommand(); if (!$cursor) { $this->fetchmore = false; @@ -137,8 +138,9 @@ abstract class CursorBasedIterator implements \Iterator } /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function rewind() { $this->reset(); @@ -146,27 +148,30 @@ abstract class CursorBasedIterator implements \Iterator } /** - * {@inheritdoc} + * @return mixed */ + #[ReturnTypeWillChange] public function current() { return $this->current; } /** - * {@inheritdoc} + * @return int|null */ + #[ReturnTypeWillChange] public function key() { return $this->position; } /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function next() { - tryFetch: { + tryFetch: if (!$this->elements && $this->fetchmore) { $this->fetch(); } @@ -178,12 +183,12 @@ abstract class CursorBasedIterator implements \Iterator } else { $this->valid = false; } - } } /** - * {@inheritdoc} + * @return bool */ + #[ReturnTypeWillChange] public function valid() { return $this->valid; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/HashKey.php b/plugins/cache-redis/Predis/Collection/Iterator/HashKey.php similarity index 84% rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/HashKey.php rename to plugins/cache-redis/Predis/Collection/Iterator/HashKey.php index aa8aeaf02..91b7d27ff 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/HashKey.php +++ b/plugins/cache-redis/Predis/Collection/Iterator/HashKey.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -17,9 +18,7 @@ use Predis\ClientInterface; * Abstracts the iteration of fields and values of an hash by leveraging the * HSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator. * - * @author Daniele Alessandri - * - * @link http://redis.io/commands/scan + * @see http://redis.io/commands/scan */ class HashKey extends CursorBasedIterator { @@ -51,6 +50,8 @@ class HashKey extends CursorBasedIterator protected function extractNext() { $this->position = key($this->elements); - $this->current = array_shift($this->elements); + $this->current = current($this->elements); + + unset($this->elements[$this->position]); } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/Keyspace.php b/plugins/cache-redis/Predis/Collection/Iterator/Keyspace.php similarity index 85% rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/Keyspace.php rename to plugins/cache-redis/Predis/Collection/Iterator/Keyspace.php index 5d985b9bc..b5fa022aa 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/Keyspace.php +++ b/plugins/cache-redis/Predis/Collection/Iterator/Keyspace.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -17,9 +18,7 @@ use Predis\ClientInterface; * Abstracts the iteration of the keyspace on a Redis instance by leveraging the * SCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator. * - * @author Daniele Alessandri - * - * @link http://redis.io/commands/scan + * @see http://redis.io/commands/scan */ class Keyspace extends CursorBasedIterator { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/ListKey.php b/plugins/cache-redis/Predis/Collection/Iterator/ListKey.php similarity index 82% rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/ListKey.php rename to plugins/cache-redis/Predis/Collection/Iterator/ListKey.php index 7a6eb479e..79ab3aa19 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/ListKey.php +++ b/plugins/cache-redis/Predis/Collection/Iterator/ListKey.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,8 +12,11 @@ namespace Predis\Collection\Iterator; +use InvalidArgumentException; +use Iterator; use Predis\ClientInterface; use Predis\NotSupportedException; +use ReturnTypeWillChange; /** * Abstracts the iteration of items stored in a list by leveraging the LRANGE @@ -24,11 +28,9 @@ use Predis\NotSupportedException; * guarantees on the returned elements because the collection can change several * times (trimmed, deleted, overwritten) during the iteration process. * - * @author Daniele Alessandri - * - * @link http://redis.io/commands/lrange + * @see http://redis.io/commands/lrange */ -class ListKey implements \Iterator +class ListKey implements Iterator { protected $client; protected $count; @@ -45,14 +47,14 @@ class ListKey implements \Iterator * @param string $key Redis list key. * @param int $count Number of items retrieved on each fetch operation. * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function __construct(ClientInterface $client, $key, $count = 10) { $this->requiredCommand($client, 'LRANGE'); if ((false === $count = filter_var($count, FILTER_VALIDATE_INT)) || $count < 0) { - throw new \InvalidArgumentException('The $count argument must be a positive integer.'); + throw new InvalidArgumentException('The $count argument must be a positive integer.'); } $this->client = $client; @@ -73,8 +75,8 @@ class ListKey implements \Iterator */ protected function requiredCommand(ClientInterface $client, $commandID) { - if (!$client->getProfile()->supportsCommand($commandID)) { - throw new NotSupportedException("The current profile does not support '$commandID'."); + if (!$client->getCommandFactory()->supports($commandID)) { + throw new NotSupportedException("'$commandID' is not supported by the current command factory."); } } @@ -85,7 +87,7 @@ class ListKey implements \Iterator { $this->valid = true; $this->fetchmore = true; - $this->elements = array(); + $this->elements = []; $this->position = -1; $this->current = null; } @@ -126,8 +128,9 @@ class ListKey implements \Iterator } /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function rewind() { $this->reset(); @@ -135,24 +138,27 @@ class ListKey implements \Iterator } /** - * {@inheritdoc} + * @return mixed */ + #[ReturnTypeWillChange] public function current() { return $this->current; } /** - * {@inheritdoc} + * @return int|null */ + #[ReturnTypeWillChange] public function key() { return $this->position; } /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function next() { if (!$this->elements && $this->fetchmore) { @@ -167,8 +173,9 @@ class ListKey implements \Iterator } /** - * {@inheritdoc} + * @return bool */ + #[ReturnTypeWillChange] public function valid() { return $this->valid; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SetKey.php b/plugins/cache-redis/Predis/Collection/Iterator/SetKey.php similarity index 86% rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SetKey.php rename to plugins/cache-redis/Predis/Collection/Iterator/SetKey.php index bf2543975..74f982346 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SetKey.php +++ b/plugins/cache-redis/Predis/Collection/Iterator/SetKey.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -17,9 +18,7 @@ use Predis\ClientInterface; * Abstracts the iteration of members stored in a set by leveraging the SSCAN * command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator. * - * @author Daniele Alessandri - * - * @link http://redis.io/commands/scan + * @see http://redis.io/commands/scan */ class SetKey extends CursorBasedIterator { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SortedSetKey.php b/plugins/cache-redis/Predis/Collection/Iterator/SortedSetKey.php similarity index 76% rename from snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SortedSetKey.php rename to plugins/cache-redis/Predis/Collection/Iterator/SortedSetKey.php index e2f178922..abee8c252 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Collection/Iterator/SortedSetKey.php +++ b/plugins/cache-redis/Predis/Collection/Iterator/SortedSetKey.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -17,9 +18,7 @@ use Predis\ClientInterface; * Abstracts the iteration of members stored in a sorted set by leveraging the * ZSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator. * - * @author Daniele Alessandri - * - * @link http://redis.io/commands/scan + * @see http://redis.io/commands/scan */ class SortedSetKey extends CursorBasedIterator { @@ -50,11 +49,9 @@ class SortedSetKey extends CursorBasedIterator */ protected function extractNext() { - if ($kv = each($this->elements)) { - $this->position = $kv[0]; - $this->current = $kv[1]; + $this->position = key($this->elements); + $this->current = current($this->elements); - unset($this->elements[$this->position]); - } + unset($this->elements[$this->position]); } } diff --git a/plugins/cache-redis/Predis/Command/Argument/ArrayableArgument.php b/plugins/cache-redis/Predis/Command/Argument/ArrayableArgument.php new file mode 100644 index 000000000..11073c054 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/ArrayableArgument.php @@ -0,0 +1,26 @@ +unit = $unit; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByBox.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByBox.php new file mode 100644 index 000000000..7dd9f23b7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByBox.php @@ -0,0 +1,43 @@ +width = $width; + $this->height = $height; + $this->setUnit($unit); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->width, $this->height, $this->unit]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByInterface.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByInterface.php new file mode 100644 index 000000000..767886cef --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/ByInterface.php @@ -0,0 +1,19 @@ +radius = $radius; + $this->setUnit($unit); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->radius, $this->unit]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromInterface.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromInterface.php new file mode 100644 index 000000000..44700bda4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromInterface.php @@ -0,0 +1,19 @@ +longitude = $longitude; + $this->latitude = $latitude; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->longitude, $this->latitude]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromMember.php b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromMember.php new file mode 100644 index 000000000..9e24b2ba0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Geospatial/FromMember.php @@ -0,0 +1,36 @@ +member = $member; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->member]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/AggregateArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/AggregateArguments.php new file mode 100644 index 000000000..950967070 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/AggregateArguments.php @@ -0,0 +1,161 @@ + 'ASC', + 'desc' => 'DESC', + ]; + + /** + * Loads document attributes from the source document. + * + * @param string ...$fields Could be just '*' to load all fields + * @return $this + */ + public function load(string ...$fields): self + { + $arguments = func_get_args(); + + $this->arguments[] = 'LOAD'; + + if ($arguments[0] === '*') { + $this->arguments[] = '*'; + + return $this; + } + + $this->arguments[] = count($arguments); + $this->arguments = array_merge($this->arguments, $arguments); + + return $this; + } + + /** + * Loads document attributes from the source document. + * + * @param string ...$properties + * @return $this + */ + public function groupBy(string ...$properties): self + { + $arguments = func_get_args(); + + array_push($this->arguments, 'GROUPBY', count($arguments)); + $this->arguments = array_merge($this->arguments, $arguments); + + return $this; + } + + /** + * Groups the results in the pipeline based on one or more properties. + * + * If you want to add alias property to your argument just add "true" value in arguments enumeration, + * next value will be considered as alias to previous one. + * + * Example: 'argument', true, 'name' => 'argument' AS 'name' + * + * @param string $function + * @param string|bool ...$argument + * @return $this + */ + public function reduce(string $function, ...$argument): self + { + $arguments = func_get_args(); + $functionValue = array_shift($arguments); + $argumentsCounter = 0; + + for ($i = 0, $iMax = count($arguments); $i < $iMax; $i++) { + if (true === $arguments[$i]) { + $arguments[$i] = 'AS'; + $i++; + continue; + } + + $argumentsCounter++; + } + + array_push($this->arguments, 'REDUCE', $functionValue); + $this->arguments = array_merge($this->arguments, [$argumentsCounter], $arguments); + + return $this; + } + + /** + * Sorts the pipeline up until the point of SORTBY, using a list of properties. + * + * @param int $max + * @param string ...$properties Enumeration of properties, including sorting direction (ASC, DESC) + * @return $this + */ + public function sortBy(int $max = 0, ...$properties): self + { + $arguments = func_get_args(); + $maxValue = array_shift($arguments); + + $this->arguments[] = 'SORTBY'; + $this->arguments = array_merge($this->arguments, [count($arguments)], $arguments); + + if ($maxValue !== 0) { + array_push($this->arguments, 'MAX', $maxValue); + } + + return $this; + } + + /** + * Applies a 1-to-1 transformation on one or more properties and either stores the result + * as a new property down the pipeline or replaces any property using this transformation. + * + * @param string $expression + * @param string $as + * @return $this + */ + public function apply(string $expression, string $as = ''): self + { + array_push($this->arguments, 'APPLY', $expression); + + if ($as !== '') { + array_push($this->arguments, 'AS', $as); + } + + return $this; + } + + /** + * Scan part of the results with a quicker alternative than LIMIT. + * + * @param int $readSize + * @param int $idleTime + * @return $this + */ + public function withCursor(int $readSize = 0, int $idleTime = 0): self + { + $this->arguments[] = 'WITHCURSOR'; + + if ($readSize !== 0) { + array_push($this->arguments, 'COUNT', $readSize); + } + + if ($idleTime !== 0) { + array_push($this->arguments, 'MAXIDLE', $idleTime); + } + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/AlterArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/AlterArguments.php new file mode 100644 index 000000000..5acd2fe1e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/AlterArguments.php @@ -0,0 +1,17 @@ +arguments[] = 'LANGUAGE'; + $this->arguments[] = $defaultLanguage; + + return $this; + } + + /** + * Selects the dialect version under which to execute the query. + * If not specified, the query will execute under the default dialect version + * set during module initial loading or via FT.CONFIG SET command. + * + * @param string $dialect + * @return $this + */ + public function dialect(string $dialect): self + { + $this->arguments[] = 'DIALECT'; + $this->arguments[] = $dialect; + + return $this; + } + + /** + * If set, does not scan and index. + * + * @return $this + */ + public function skipInitialScan(): self + { + $this->arguments[] = 'SKIPINITIALSCAN'; + + return $this; + } + + /** + * Adds an arbitrary, binary safe payload that is exposed to custom scoring functions. + * + * @param string $payload + * @return $this + */ + public function payload(string $payload): self + { + $this->arguments[] = 'PAYLOAD'; + $this->arguments[] = $payload; + + return $this; + } + + /** + * Also returns the relative internal score of each document. + * + * @return $this + */ + public function withScores(): self + { + $this->arguments[] = 'WITHSCORES'; + + return $this; + } + + /** + * Retrieves optional document payloads. + * + * @return $this + */ + public function withPayloads(): self + { + $this->arguments[] = 'WITHPAYLOADS'; + + return $this; + } + + /** + * Does not try to use stemming for query expansion but searches the query terms verbatim. + * + * @return $this + */ + public function verbatim(): self + { + $this->arguments[] = 'VERBATIM'; + + return $this; + } + + /** + * Overrides the timeout parameter of the module. + * + * @param int $timeout + * @return $this + */ + public function timeout(int $timeout): self + { + $this->arguments[] = 'TIMEOUT'; + $this->arguments[] = $timeout; + + return $this; + } + + /** + * Adds an arbitrary, binary safe payload that is exposed to custom scoring functions. + * + * @param int $offset + * @param int $num + * @return $this + */ + public function limit(int $offset, int $num): self + { + array_push($this->arguments, 'LIMIT', $offset, $num); + + return $this; + } + + /** + * Adds filter expression into index. + * + * @param string $filter + * @return $this + */ + public function filter(string $filter): self + { + $this->arguments[] = 'FILTER'; + $this->arguments[] = $filter; + + return $this; + } + + /** + * Defines one or more value parameters. Each parameter has a name and a value. + * + * Example: ['name1', 'value1', 'name2', 'value2'...] + * + * @param array $nameValuesDictionary + * @return $this + */ + public function params(array $nameValuesDictionary): self + { + $this->arguments[] = 'PARAMS'; + $this->arguments[] = count($nameValuesDictionary); + $this->arguments = array_merge($this->arguments, $nameValuesDictionary); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/CreateArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/CreateArguments.php new file mode 100644 index 000000000..b8e0176ba --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/CreateArguments.php @@ -0,0 +1,191 @@ + 'HASH', + 'json' => 'JSON', + ]; + + /** + * Specify data type for given index. To index JSON you must have the RedisJSON module to be installed. + * + * @param string $modifier + * @return $this + */ + public function on(string $modifier = 'HASH'): self + { + if (in_array(strtoupper($modifier), $this->supportedDataTypesEnum)) { + $this->arguments[] = 'ON'; + $this->arguments[] = $this->supportedDataTypesEnum[strtolower($modifier)]; + + return $this; + } + + $enumValues = implode(', ', array_values($this->supportedDataTypesEnum)); + throw new InvalidArgumentException("Wrong modifier value given. Currently supports: {$enumValues}"); + } + + /** + * Adds one or more prefixes into index. + * + * @param array $prefixes + * @return $this + */ + public function prefix(array $prefixes): self + { + $this->arguments[] = 'PREFIX'; + $this->arguments[] = count($prefixes); + $this->arguments = array_merge($this->arguments, $prefixes); + + return $this; + } + + /** + * Document attribute set as document language. + * + * @param string $languageAttribute + * @return $this + */ + public function languageField(string $languageAttribute): self + { + $this->arguments[] = 'LANGUAGE_FIELD'; + $this->arguments[] = $languageAttribute; + + return $this; + } + + /** + * Default score for documents in the index. + * + * @param float $defaultScore + * @return $this + */ + public function score(float $defaultScore = 1.0): self + { + $this->arguments[] = 'SCORE'; + $this->arguments[] = $defaultScore; + + return $this; + } + + /** + * Document attribute that used as the document rank based on the user ranking. + * + * @param string $scoreAttribute + * @return $this + */ + public function scoreField(string $scoreAttribute): self + { + $this->arguments[] = 'SCORE_FIELD'; + $this->arguments[] = $scoreAttribute; + + return $this; + } + + /** + * Forces RediSearch to encode indexes as if there were more than 32 text attributes. + * + * @return $this + */ + public function maxTextFields(): self + { + $this->arguments[] = 'MAXTEXTFIELDS'; + + return $this; + } + + /** + * Does not store term offsets for documents. + * + * @return $this + */ + public function noOffsets(): self + { + $this->arguments[] = 'NOOFFSETS'; + + return $this; + } + + /** + * Creates a lightweight temporary index that expires after a specified period of inactivity, in seconds. + * + * @param int $seconds + * @return $this + */ + public function temporary(int $seconds): self + { + $this->arguments[] = 'TEMPORARY'; + $this->arguments[] = $seconds; + + return $this; + } + + /** + * Conserves storage space and memory by disabling highlighting support. + * + * @return $this + */ + public function noHl(): self + { + $this->arguments[] = 'NOHL'; + + return $this; + } + + /** + * Does not store attribute bits for each term. + * + * @return $this + */ + public function noFields(): self + { + $this->arguments[] = 'NOFIELDS'; + + return $this; + } + + /** + * Avoids saving the term frequencies in the index. + * + * @return $this + */ + public function noFreqs(): self + { + $this->arguments[] = 'NOFREQS'; + + return $this; + } + + /** + * Sets the index with a custom stopword list, to be ignored during indexing and search time. + * + * @param array $stopWords + * @return $this + */ + public function stopWords(array $stopWords): self + { + $this->arguments[] = 'STOPWORDS'; + $this->arguments[] = count($stopWords); + $this->arguments = array_merge($this->arguments, $stopWords); + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/CursorArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/CursorArguments.php new file mode 100644 index 000000000..a8bd6b56f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/CursorArguments.php @@ -0,0 +1,44 @@ +arguments, 'COUNT', $readSize); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/DropArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/DropArguments.php new file mode 100644 index 000000000..0c6313201 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/DropArguments.php @@ -0,0 +1,43 @@ +arguments[] = 'DD'; + + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/ExplainArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/ExplainArguments.php new file mode 100644 index 000000000..b4bd235b9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/ExplainArguments.php @@ -0,0 +1,17 @@ +arguments[] = 'SEARCH'; + + return $this; + } + + /** + * Adds aggregate context. + * + * @return $this + */ + public function aggregate(): self + { + $this->arguments[] = 'AGGREGATE'; + + return $this; + } + + /** + * Removes details of reader iterator. + * + * @return $this + */ + public function limited(): self + { + $this->arguments[] = 'LIMITED'; + + return $this; + } + + /** + * Is query string, as if sent to FT.SEARCH. + * + * @param string $query + * @return $this + */ + public function query(string $query): self + { + $this->arguments[] = 'QUERY'; + $this->arguments[] = $query; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/AbstractField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/AbstractField.php new file mode 100644 index 000000000..eb49f0995 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/AbstractField.php @@ -0,0 +1,69 @@ +fieldArguments[] = $identifier; + + if ($alias !== '') { + $this->fieldArguments[] = 'AS'; + $this->fieldArguments[] = $alias; + } + + $this->fieldArguments[] = $fieldType; + + if ($sortable === self::SORTABLE) { + $this->fieldArguments[] = 'SORTABLE'; + } elseif ($sortable === self::SORTABLE_UNF) { + $this->fieldArguments[] = 'SORTABLE'; + $this->fieldArguments[] = 'UNF'; + } + + if ($noIndex) { + $this->fieldArguments[] = 'NOINDEX'; + } + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->fieldArguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/FieldInterface.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/FieldInterface.php new file mode 100644 index 000000000..80e57eba0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/FieldInterface.php @@ -0,0 +1,22 @@ +setCommonOptions('GEO', $identifier, $alias, $sortable, $noIndex); + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/NumericField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/NumericField.php new file mode 100644 index 000000000..758b4e9c6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/NumericField.php @@ -0,0 +1,31 @@ +setCommonOptions('NUMERIC', $identifier, $alias, $sortable, $noIndex); + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TagField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TagField.php new file mode 100644 index 000000000..358b3090e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TagField.php @@ -0,0 +1,44 @@ +setCommonOptions('TAG', $identifier, $alias, $sortable, $noIndex); + + if ($separator !== ',') { + $this->fieldArguments[] = 'SEPARATOR'; + $this->fieldArguments[] = $separator; + } + + if ($caseSensitive) { + $this->fieldArguments[] = 'CASESENSITIVE'; + } + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TextField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TextField.php new file mode 100644 index 000000000..d72c62383 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/TextField.php @@ -0,0 +1,57 @@ +setCommonOptions('TEXT', $identifier, $alias, $sortable, $noIndex); + + if ($noStem) { + $this->fieldArguments[] = 'NOSTEM'; + } + + if ($phonetic !== '') { + $this->fieldArguments[] = 'PHONETIC'; + $this->fieldArguments[] = $phonetic; + } + + if ($weight !== 1) { + $this->fieldArguments[] = 'WEIGHT'; + $this->fieldArguments[] = $weight; + } + + if ($withSuffixTrie) { + $this->fieldArguments[] = 'WITHSUFFIXTRIE'; + } + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/VectorField.php b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/VectorField.php new file mode 100644 index 000000000..5228c2250 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SchemaFields/VectorField.php @@ -0,0 +1,47 @@ +setCommonOptions('VECTOR', $fieldName, $alias); + + array_push($this->fieldArguments, $algorithm, count($attributeNameValueDictionary)); + $this->fieldArguments = array_merge($this->fieldArguments, $attributeNameValueDictionary); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->fieldArguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SearchArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SearchArguments.php new file mode 100644 index 000000000..d1eb70580 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SearchArguments.php @@ -0,0 +1,306 @@ + 'ASC', + 'desc' => 'DESC', + ]; + + /** + * Returns the document ids and not the content. + * + * @return $this + */ + public function noContent(): self + { + $this->arguments[] = 'NOCONTENT'; + + return $this; + } + + /** + * Returns the value of the sorting key, right after the id and score and/or payload, if requested. + * + * @return $this + */ + public function withSortKeys(): self + { + $this->arguments[] = 'WITHSORTKEYS'; + + return $this; + } + + /** + * Limits results to those having numeric values ranging between min and max, + * if numeric_attribute is defined as a numeric attribute in FT.CREATE. + * Min and max follow ZRANGE syntax, and can be -inf, +inf, and use( for exclusive ranges. + * Multiple numeric filters for different attributes are supported in one query. + * + * @param array ...$filter Should contain: numeric_field, min and max. Example: ['numeric_field', 1, 10] + * @return $this + */ + public function searchFilter(array ...$filter): self + { + $arguments = func_get_args(); + + foreach ($arguments as $argument) { + array_push($this->arguments, 'FILTER', ...$argument); + } + + return $this; + } + + /** + * Filter the results to a given radius from lon and lat. Radius is given as a number and units. + * + * @param array ...$filter Should contain: geo_field, lon, lat, radius, unit. Example: ['geo_field', 34.1231, 35.1231, 300, km] + * @return $this + */ + public function geoFilter(array ...$filter): self + { + $arguments = func_get_args(); + + foreach ($arguments as $argument) { + array_push($this->arguments, 'GEOFILTER', ...$argument); + } + + return $this; + } + + /** + * Limits the result to a given set of keys specified in the list. + * + * @param array $keys + * @return $this + */ + public function inKeys(array $keys): self + { + $this->arguments[] = 'INKEYS'; + $this->arguments[] = count($keys); + $this->arguments = array_merge($this->arguments, $keys); + + return $this; + } + + /** + * Filters the results to those appearing only in specific attributes of the document, like title or URL. + * + * @param array $fields + * @return $this + */ + public function inFields(array $fields): self + { + $this->arguments[] = 'INFIELDS'; + $this->arguments[] = count($fields); + $this->arguments = array_merge($this->arguments, $fields); + + return $this; + } + + /** + * Limits the attributes returned from the document. + * Num is the number of attributes following the keyword. + * If num is 0, it acts like NOCONTENT. + * Identifier is either an attribute name (for hashes and JSON) or a JSON Path expression (for JSON). + * Property is an optional name used in the result. If not provided, the identifier is used in the result. + * + * If you want to add alias property to your identifier just add "true" value in identifier enumeration, + * next value will be considered as alias to previous one. + * + * Example: 'identifier', true, 'property' => 'identifier' AS 'property' + * + * @param int $count + * @param string|bool ...$identifier + * @return $this + */ + public function addReturn(int $count, ...$identifier): self + { + $arguments = func_get_args(); + + $this->arguments[] = 'RETURN'; + + for ($i = 1, $iMax = count($arguments); $i < $iMax; $i++) { + if (true === $arguments[$i]) { + $arguments[$i] = 'AS'; + } + } + + $this->arguments = array_merge($this->arguments, $arguments); + + return $this; + } + + /** + * Returns only the sections of the attribute that contain the matched text. + * + * @param array $fields + * @param int $frags + * @param int $len + * @param string $separator + * @return $this + */ + public function summarize(array $fields = [], int $frags = 0, int $len = 0, string $separator = ''): self + { + $this->arguments[] = 'SUMMARIZE'; + + if (!empty($fields)) { + $this->arguments[] = 'FIELDS'; + $this->arguments[] = count($fields); + $this->arguments = array_merge($this->arguments, $fields); + } + + if ($frags !== 0) { + $this->arguments[] = 'FRAGS'; + $this->arguments[] = $frags; + } + + if ($len !== 0) { + $this->arguments[] = 'LEN'; + $this->arguments[] = $len; + } + + if ($separator !== '') { + $this->arguments[] = 'SEPARATOR'; + $this->arguments[] = $separator; + } + + return $this; + } + + /** + * Formats occurrences of matched text. + * + * @param array $fields + * @param string $openTag + * @param string $closeTag + * @return $this + */ + public function highlight(array $fields = [], string $openTag = '', string $closeTag = ''): self + { + $this->arguments[] = 'HIGHLIGHT'; + + if (!empty($fields)) { + $this->arguments[] = 'FIELDS'; + $this->arguments[] = count($fields); + $this->arguments = array_merge($this->arguments, $fields); + } + + if ($openTag !== '' && $closeTag !== '') { + array_push($this->arguments, 'TAGS', $openTag, $closeTag); + } + + return $this; + } + + /** + * Allows a maximum of N intervening number of unmatched offsets between phrase terms. + * In other words, the slop for exact phrases is 0. + * + * @param int $slop + * @return $this + */ + public function slop(int $slop): self + { + $this->arguments[] = 'SLOP'; + $this->arguments[] = $slop; + + return $this; + } + + /** + * Puts the query terms in the same order in the document as in the query, regardless of the offsets between them. + * Typically used in conjunction with SLOP. + * + * @return $this + */ + public function inOrder(): self + { + $this->arguments[] = 'INORDER'; + + return $this; + } + + /** + * Uses a custom query expander instead of the stemmer. + * + * @param string $expander + * @return $this + */ + public function expander(string $expander): self + { + $this->arguments[] = 'EXPANDER'; + $this->arguments[] = $expander; + + return $this; + } + + /** + * Uses a custom scoring function you define. + * + * @param string $scorer + * @return $this + */ + public function scorer(string $scorer): self + { + $this->arguments[] = 'SCORER'; + $this->arguments[] = $scorer; + + return $this; + } + + /** + * Returns a textual description of how the scores were calculated. + * Using this options requires the WITHSCORES option. + * + * @return $this + */ + public function explainScore(): self + { + $this->arguments[] = 'EXPLAINSCORE'; + + return $this; + } + + /** + * Orders the results by the value of this attribute. + * This applies to both text and numeric attributes. + * Attributes needed for SORTBY should be declared as SORTABLE in the index, in order to be available with very low latency. + * Note that this adds memory overhead. + * + * @param string $sortAttribute + * @param string $orderBy + * @return $this + */ + public function sortBy(string $sortAttribute, string $orderBy = 'asc'): self + { + $this->arguments[] = 'SORTBY'; + $this->arguments[] = $sortAttribute; + + if (in_array(strtoupper($orderBy), $this->sortingEnum)) { + $this->arguments[] = $this->sortingEnum[strtolower($orderBy)]; + } else { + $enumValues = implode(', ', array_values($this->sortingEnum)); + throw new InvalidArgumentException("Wrong order direction value given. Currently supports: {$enumValues}"); + } + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SpellcheckArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SpellcheckArguments.php new file mode 100644 index 000000000..7a6c24891 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SpellcheckArguments.php @@ -0,0 +1,59 @@ + 'INCLUDE', + 'exclude' => 'EXCLUDE', + ]; + + /** + * Is maximum Levenshtein distance for spelling suggestions (default: 1, max: 4). + * + * @return $this + */ + public function distance(int $distance): self + { + $this->arguments[] = 'DISTANCE'; + $this->arguments[] = $distance; + + return $this; + } + + /** + * Specifies an inclusion (INCLUDE) or exclusion (EXCLUDE) of a custom dictionary named {dict}. + * + * @param string $dictionary + * @param string $modifier + * @param string ...$terms + * @return $this + */ + public function terms(string $dictionary, string $modifier = 'INCLUDE', string ...$terms): self + { + if (!in_array(strtoupper($modifier), $this->termsEnum)) { + $enumValues = implode(', ', array_values($this->termsEnum)); + throw new InvalidArgumentException("Wrong modifier value given. Currently supports: {$enumValues}"); + } + + array_push($this->arguments, 'TERMS', $this->termsEnum[strtolower($modifier)], $dictionary, ...$terms); + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SugAddArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SugAddArguments.php new file mode 100644 index 000000000..c8b976978 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SugAddArguments.php @@ -0,0 +1,28 @@ +arguments[] = 'INCR'; + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SugGetArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SugGetArguments.php new file mode 100644 index 000000000..1176c7736 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SugGetArguments.php @@ -0,0 +1,41 @@ +arguments[] = 'FUZZY'; + + return $this; + } + + /** + * Limits the results to a maximum of num (default: 5). + * + * @param int $num + * @return $this + */ + public function max(int $num): self + { + array_push($this->arguments, 'MAX', $num); + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Search/SynUpdateArguments.php b/plugins/cache-redis/Predis/Command/Argument/Search/SynUpdateArguments.php new file mode 100644 index 000000000..a6b286a48 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Search/SynUpdateArguments.php @@ -0,0 +1,17 @@ +offset = $offset; + $this->count = $count; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->offset, $this->count]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/Server/To.php b/plugins/cache-redis/Predis/Command/Argument/Server/To.php new file mode 100644 index 000000000..1d77ef681 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/Server/To.php @@ -0,0 +1,57 @@ +host = $host; + $this->port = $port; + $this->isForce = $isForce; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + $arguments = [self::KEYWORD, $this->host, $this->port]; + + if ($this->isForce) { + $arguments[] = self::FORCE_KEYWORD; + } + + return $arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AddArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AddArguments.php new file mode 100644 index 000000000..a9fe6f79f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AddArguments.php @@ -0,0 +1,30 @@ +arguments, 'ON_DUPLICATE', $policy); + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AlterArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AlterArguments.php new file mode 100644 index 000000000..238ebc36d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/AlterArguments.php @@ -0,0 +1,17 @@ +arguments, 'RETENTION', $retentionPeriod); + + return $this; + } + + /** + * Is initial allocation size, in bytes, for the data part of each new chunk. + * + * @param int $size + * @return $this + */ + public function chunkSize(int $size): self + { + array_push($this->arguments, 'CHUNK_SIZE', $size); + + return $this; + } + + /** + * Is policy for handling insertion of multiple samples with identical timestamps. + * + * @param string $policy + * @return $this + */ + public function duplicatePolicy(string $policy = self::POLICY_BLOCK): self + { + array_push($this->arguments, 'DUPLICATE_POLICY', $policy); + + return $this; + } + + /** + * Is set of label-value pairs that represent metadata labels of the key and serve as a secondary index. + * + * @param mixed ...$labelValuePair + * @return $this + */ + public function labels(...$labelValuePair): self + { + array_push($this->arguments, 'LABELS', ...$labelValuePair); + + return $this; + } + + /** + * Specifies the series samples encoding format. + * + * @param string $encoding + * @return $this + */ + public function encoding(string $encoding = self::ENCODING_COMPRESSED): self + { + array_push($this->arguments, 'ENCODING', $encoding); + + return $this; + } + + /** + * Is used when a time series is a compaction. + * With LATEST, TS.GET reports the compacted value of the latest, possibly partial, bucket. + * + * @return $this + */ + public function latest(): self + { + $this->arguments[] = 'LATEST'; + + return $this; + } + + /** + * Includes in the reply all label-value pairs representing metadata labels of the time series. + * + * @return $this + */ + public function withLabels(): self + { + $this->arguments[] = 'WITHLABELS'; + + return $this; + } + + /** + * Returns a subset of the label-value pairs that represent metadata labels of the time series. + * + * @return $this + */ + public function selectedLabels(string ...$labels): self + { + array_push($this->arguments, 'SELECTED_LABELS', ...$labels); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CreateArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CreateArguments.php new file mode 100644 index 000000000..e47d8ef72 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/CreateArguments.php @@ -0,0 +1,17 @@ +arguments, 'TIMESTAMP', $timeStamp); + + return $this; + } + + /** + * Changes data storage from compressed (default) to uncompressed. + * + * @return $this + */ + public function uncompressed(): self + { + $this->arguments[] = 'UNCOMPRESSED'; + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/InfoArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/InfoArguments.php new file mode 100644 index 000000000..1b2cec664 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/InfoArguments.php @@ -0,0 +1,43 @@ +arguments[] = 'DEBUG'; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->arguments; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MGetArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MGetArguments.php new file mode 100644 index 000000000..585f574e6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/MGetArguments.php @@ -0,0 +1,17 @@ +arguments, 'FILTER', ...$filterExpressions); + + return $this; + } + + /** + * Splits time series into groups, each group contains time series that share the same + * value for the provided label name, then aggregates results in each group. + * + * @param string $label + * @param string $reducer + * @return $this + */ + public function groupBy(string $label, string $reducer): self + { + array_push($this->arguments, 'GROUPBY', $label, 'REDUCE', $reducer); + + return $this; + } +} diff --git a/plugins/cache-redis/Predis/Command/Argument/TimeSeries/RangeArguments.php b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/RangeArguments.php new file mode 100644 index 000000000..00d3772d7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Argument/TimeSeries/RangeArguments.php @@ -0,0 +1,85 @@ +arguments, 'FILTER_BY_TS', ...$ts); + + return $this; + } + + /** + * Filters samples by minimum and maximum values. + * + * @param int $min + * @param int $max + * @return $this + */ + public function filterByValue(int $min, int $max): self + { + array_push($this->arguments, 'FILTER_BY_VALUE', $min, $max); + + return $this; + } + + /** + * Limits the number of returned samples. + * + * @param int $count + * @return $this + */ + public function count(int $count): self + { + array_push($this->arguments, 'COUNT', $count); + + return $this; + } + + /** + * Aggregates samples into time buckets. + * + * @param string $aggregator + * @param int $bucketDuration Is duration of each bucket, in milliseconds. + * @param int $align It controls the time bucket timestamps by changing the reference timestamp on which a bucket is defined. + * @param int $bucketTimestamp Controls how bucket timestamps are reported. + * @param bool $empty Is a flag, which, when specified, reports aggregations also for empty buckets. + * @return $this + */ + public function aggregation(string $aggregator, int $bucketDuration, int $align = 0, int $bucketTimestamp = 0, bool $empty = false): self + { + if ($align > 0) { + array_push($this->arguments, 'ALIGN', $align); + } + + array_push($this->arguments, 'AGGREGATION', $aggregator, $bucketDuration); + + if ($bucketTimestamp > 0) { + array_push($this->arguments, 'BUCKETTIMESTAMP', $bucketTimestamp); + } + + if (true === $empty) { + $this->arguments[] = 'EMPTY'; + } + + return $this; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/Command.php b/plugins/cache-redis/Predis/Command/Command.php similarity index 75% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/Command.php rename to plugins/cache-redis/Predis/Command/Command.php index bb538e7c5..68629c454 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/Command.php +++ b/plugins/cache-redis/Predis/Command/Command.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,32 +14,18 @@ namespace Predis\Command; /** * Base class for Redis commands. - * - * @author Daniele Alessandri */ abstract class Command implements CommandInterface { private $slot; - private $arguments = array(); - - /** - * Returns a filtered array of the arguments. - * - * @param array $arguments List of arguments. - * - * @return array - */ - protected function filterArguments(array $arguments) - { - return $arguments; - } + private $arguments = []; /** * {@inheritdoc} */ public function setArguments(array $arguments) { - $this->arguments = $this->filterArguments($arguments); + $this->arguments = $arguments; unset($this->slot); } @@ -82,9 +69,7 @@ abstract class Command implements CommandInterface */ public function getSlot() { - if (isset($this->slot)) { - return $this->slot; - } + return $this->slot ?? null; } /** @@ -104,7 +89,7 @@ abstract class Command implements CommandInterface */ public static function normalizeArguments(array $arguments) { - if (count($arguments) === 1 && is_array($arguments[0])) { + if (count($arguments) === 1 && isset($arguments[0]) && is_array($arguments[0])) { return $arguments[0]; } @@ -121,9 +106,21 @@ abstract class Command implements CommandInterface public static function normalizeVariadic(array $arguments) { if (count($arguments) === 2 && is_array($arguments[1])) { - return array_merge(array($arguments[0]), $arguments[1]); + return array_merge([$arguments[0]], $arguments[1]); } return $arguments; } + + /** + * Remove all false values from arguments. + * + * @return void + */ + public function filterArguments(): void + { + $this->arguments = array_filter($this->arguments, static function ($argument) { + return $argument !== false && $argument !== null; + }); + } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/CommandInterface.php b/plugins/cache-redis/Predis/Command/CommandInterface.php similarity index 90% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/CommandInterface.php rename to plugins/cache-redis/Predis/Command/CommandInterface.php index 9f349e1df..20480316d 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/CommandInterface.php +++ b/plugins/cache-redis/Predis/Command/CommandInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,8 +14,6 @@ namespace Predis\Command; /** * Defines an abstraction representing a Redis command. - * - * @author Daniele Alessandri */ interface CommandInterface { @@ -73,7 +72,7 @@ interface CommandInterface /** * Parses a raw response and returns a PHP object. * - * @param string $data Binary string containing the whole response. + * @param string|array|null $data Binary string containing the whole response. * * @return mixed */ diff --git a/plugins/cache-redis/Predis/Command/Factory.php b/plugins/cache-redis/Predis/Command/Factory.php new file mode 100644 index 000000000..ed94dc572 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Factory.php @@ -0,0 +1,143 @@ +getCommandClass($commandID) === null) { + return false; + } + } + + return true; + } + + /** + * Returns the FQCN of a class that represents the specified command ID. + * + * @codeCoverageIgnore + * + * @param string $commandID Command ID + * + * @return string|null + */ + public function getCommandClass(string $commandID): ?string + { + return $this->commands[strtoupper($commandID)] ?? null; + } + + /** + * {@inheritdoc} + */ + public function create(string $commandID, array $arguments = []): CommandInterface + { + if (!$commandClass = $this->getCommandClass($commandID)) { + $commandID = strtoupper($commandID); + + throw new ClientException("Command `$commandID` is not a registered Redis command."); + } + + $command = new $commandClass(); + $command->setArguments($arguments); + + if (isset($this->processor)) { + $this->processor->process($command); + } + + return $command; + } + + /** + * Defines a command in the factory. + * + * Only classes implementing Predis\Command\CommandInterface are allowed to + * handle a command. If the command specified by its ID is already handled + * by the factory, the underlying command class is replaced by the new one. + * + * @param string $commandID Command ID + * @param string $commandClass FQCN of a class implementing Predis\Command\CommandInterface + * + * @throws InvalidArgumentException + */ + public function define(string $commandID, string $commandClass): void + { + if (!is_a($commandClass, 'Predis\Command\CommandInterface', true)) { + throw new InvalidArgumentException( + "Class $commandClass must implement Predis\Command\CommandInterface" + ); + } + + $this->commands[strtoupper($commandID)] = $commandClass; + } + + /** + * Undefines a command in the factory. + * + * When the factory already has a class handler associated to the specified + * command ID it is removed from the map of known commands. Nothing happens + * when the command is not handled by the factory. + * + * @param string $commandID Command ID + */ + public function undefine(string $commandID): void + { + unset($this->commands[strtoupper($commandID)]); + } + + /** + * Sets a command processor for processing command arguments. + * + * Command processors are used to process and transform arguments of Redis + * commands before their newly created instances are returned to the caller + * of "create()". + * + * A NULL value can be used to effectively unset any processor if previously + * set for the command factory. + * + * @param ProcessorInterface|null $processor Command processor or NULL value. + */ + public function setProcessor(?ProcessorInterface $processor): void + { + $this->processor = $processor; + } + + /** + * Returns the current command processor. + * + * @return ProcessorInterface|null + */ + public function getProcessor(): ?ProcessorInterface + { + return $this->processor; + } +} diff --git a/plugins/cache-redis/Predis/Command/FactoryInterface.php b/plugins/cache-redis/Predis/Command/FactoryInterface.php new file mode 100644 index 000000000..e81951086 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/FactoryInterface.php @@ -0,0 +1,42 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,8 +14,6 @@ namespace Predis\Command; /** * Defines a command whose keys can be prefixed. - * - * @author Daniele Alessandri */ interface PrefixableCommandInterface extends CommandInterface { diff --git a/plugins/cache-redis/Predis/Command/Processor/KeyPrefixProcessor.php b/plugins/cache-redis/Predis/Command/Processor/KeyPrefixProcessor.php new file mode 100644 index 000000000..d7a30c880 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Processor/KeyPrefixProcessor.php @@ -0,0 +1,577 @@ +prefix = $prefix; + + $prefixFirst = static::class . '::first'; + $prefixAll = static::class . '::all'; + $prefixInterleaved = static::class . '::interleaved'; + $prefixSkipFirst = static::class . '::skipFirst'; + $prefixSkipLast = static::class . '::skipLast'; + $prefixSort = static::class . '::sort'; + $prefixEvalKeys = static::class . '::evalKeys'; + $prefixZsetStore = static::class . '::zsetStore'; + $prefixMigrate = static::class . '::migrate'; + $prefixGeoradius = static::class . '::georadius'; + + $this->commands = [ + /* ---------------- Redis 1.2 ---------------- */ + 'EXISTS' => $prefixAll, + 'DEL' => $prefixAll, + 'TYPE' => $prefixFirst, + 'KEYS' => $prefixFirst, + 'RENAME' => $prefixAll, + 'RENAMENX' => $prefixAll, + 'EXPIRE' => $prefixFirst, + 'EXPIREAT' => $prefixFirst, + 'TTL' => $prefixFirst, + 'MOVE' => $prefixFirst, + 'SORT' => $prefixSort, + 'DUMP' => $prefixFirst, + 'RESTORE' => $prefixFirst, + 'SET' => $prefixFirst, + 'SETNX' => $prefixFirst, + 'MSET' => $prefixInterleaved, + 'MSETNX' => $prefixInterleaved, + 'GET' => $prefixFirst, + 'MGET' => $prefixAll, + 'GETSET' => $prefixFirst, + 'INCR' => $prefixFirst, + 'INCRBY' => $prefixFirst, + 'DECR' => $prefixFirst, + 'DECRBY' => $prefixFirst, + 'RPUSH' => $prefixFirst, + 'LPUSH' => $prefixFirst, + 'LLEN' => $prefixFirst, + 'LRANGE' => $prefixFirst, + 'LTRIM' => $prefixFirst, + 'LINDEX' => $prefixFirst, + 'LSET' => $prefixFirst, + 'LREM' => $prefixFirst, + 'LPOP' => $prefixFirst, + 'RPOP' => $prefixFirst, + 'RPOPLPUSH' => $prefixAll, + 'SADD' => $prefixFirst, + 'SREM' => $prefixFirst, + 'SPOP' => $prefixFirst, + 'SMOVE' => $prefixSkipLast, + 'SCARD' => $prefixFirst, + 'SISMEMBER' => $prefixFirst, + 'SINTER' => $prefixAll, + 'SINTERSTORE' => $prefixAll, + 'SUNION' => $prefixAll, + 'SUNIONSTORE' => $prefixAll, + 'SDIFF' => $prefixAll, + 'SDIFFSTORE' => $prefixAll, + 'SMEMBERS' => $prefixFirst, + 'SRANDMEMBER' => $prefixFirst, + 'ZADD' => $prefixFirst, + 'ZINCRBY' => $prefixFirst, + 'ZREM' => $prefixFirst, + 'ZRANGE' => $prefixFirst, + 'ZREVRANGE' => $prefixFirst, + 'ZRANGEBYSCORE' => $prefixFirst, + 'ZCARD' => $prefixFirst, + 'ZSCORE' => $prefixFirst, + 'ZREMRANGEBYSCORE' => $prefixFirst, + /* ---------------- Redis 2.0 ---------------- */ + 'SETEX' => $prefixFirst, + 'APPEND' => $prefixFirst, + 'SUBSTR' => $prefixFirst, + 'BLPOP' => $prefixSkipLast, + 'BRPOP' => $prefixSkipLast, + 'ZUNIONSTORE' => $prefixZsetStore, + 'ZINTERSTORE' => $prefixZsetStore, + 'ZCOUNT' => $prefixFirst, + 'ZRANK' => $prefixFirst, + 'ZREVRANK' => $prefixFirst, + 'ZREMRANGEBYRANK' => $prefixFirst, + 'HSET' => $prefixFirst, + 'HSETNX' => $prefixFirst, + 'HMSET' => $prefixFirst, + 'HINCRBY' => $prefixFirst, + 'HGET' => $prefixFirst, + 'HMGET' => $prefixFirst, + 'HDEL' => $prefixFirst, + 'HEXISTS' => $prefixFirst, + 'HLEN' => $prefixFirst, + 'HKEYS' => $prefixFirst, + 'HVALS' => $prefixFirst, + 'HGETALL' => $prefixFirst, + 'SUBSCRIBE' => $prefixAll, + 'UNSUBSCRIBE' => $prefixAll, + 'PSUBSCRIBE' => $prefixAll, + 'PUNSUBSCRIBE' => $prefixAll, + 'PUBLISH' => $prefixFirst, + /* ---------------- Redis 2.2 ---------------- */ + 'PERSIST' => $prefixFirst, + 'STRLEN' => $prefixFirst, + 'SETRANGE' => $prefixFirst, + 'GETRANGE' => $prefixFirst, + 'SETBIT' => $prefixFirst, + 'GETBIT' => $prefixFirst, + 'RPUSHX' => $prefixFirst, + 'LPUSHX' => $prefixFirst, + 'LINSERT' => $prefixFirst, + 'BRPOPLPUSH' => $prefixSkipLast, + 'ZREVRANGEBYSCORE' => $prefixFirst, + 'WATCH' => $prefixAll, + /* ---------------- Redis 2.6 ---------------- */ + 'PTTL' => $prefixFirst, + 'PEXPIRE' => $prefixFirst, + 'PEXPIREAT' => $prefixFirst, + 'PSETEX' => $prefixFirst, + 'INCRBYFLOAT' => $prefixFirst, + 'BITOP' => $prefixSkipFirst, + 'BITCOUNT' => $prefixFirst, + 'HINCRBYFLOAT' => $prefixFirst, + 'EVAL' => $prefixEvalKeys, + 'EVALSHA' => $prefixEvalKeys, + 'MIGRATE' => $prefixMigrate, + /* ---------------- Redis 2.8 ---------------- */ + 'SSCAN' => $prefixFirst, + 'ZSCAN' => $prefixFirst, + 'HSCAN' => $prefixFirst, + 'PFADD' => $prefixFirst, + 'PFCOUNT' => $prefixAll, + 'PFMERGE' => $prefixAll, + 'ZLEXCOUNT' => $prefixFirst, + 'ZRANGEBYLEX' => $prefixFirst, + 'ZREMRANGEBYLEX' => $prefixFirst, + 'ZREVRANGEBYLEX' => $prefixFirst, + 'BITPOS' => $prefixFirst, + /* ---------------- Redis 3.2 ---------------- */ + 'HSTRLEN' => $prefixFirst, + 'BITFIELD' => $prefixFirst, + 'GEOADD' => $prefixFirst, + 'GEOHASH' => $prefixFirst, + 'GEOPOS' => $prefixFirst, + 'GEODIST' => $prefixFirst, + 'GEORADIUS' => $prefixGeoradius, + 'GEORADIUSBYMEMBER' => $prefixGeoradius, + /* ---------------- Redis 5.0 ---------------- */ + 'XADD' => $prefixFirst, + 'XRANGE' => $prefixFirst, + 'XREVRANGE' => $prefixFirst, + 'XDEL' => $prefixFirst, + 'XLEN' => $prefixFirst, + 'XACK' => $prefixFirst, + 'XTRIM' => $prefixFirst, + + /* ---------------- Redis 6.2 ---------------- */ + 'GETDEL' => $prefixFirst, + + /* ---------------- Redis 7.0 ---------------- */ + 'EXPIRETIME' => $prefixFirst, + + /* RedisJSON */ + 'JSON.ARRAPPEND' => $prefixFirst, + 'JSON.ARRINDEX' => $prefixFirst, + 'JSON.ARRINSERT' => $prefixFirst, + 'JSON.ARRLEN' => $prefixFirst, + 'JSON.ARRPOP' => $prefixFirst, + 'JSON.ARRTRIM' => $prefixFirst, + 'JSON.CLEAR' => $prefixFirst, + 'JSON.DEBUG MEMORY' => $prefixFirst, + 'JSON.DEL' => $prefixFirst, + 'JSON.FORGET' => $prefixFirst, + 'JSON.GET' => $prefixFirst, + 'JSON.MGET' => $prefixAll, + 'JSON.NUMINCRBY' => $prefixFirst, + 'JSON.OBJKEYS' => $prefixFirst, + 'JSON.OBJLEN' => $prefixFirst, + 'JSON.RESP' => $prefixFirst, + 'JSON.SET' => $prefixFirst, + 'JSON.STRAPPEND' => $prefixFirst, + 'JSON.STRLEN' => $prefixFirst, + 'JSON.TOGGLE' => $prefixFirst, + 'JSON.TYPE' => $prefixFirst, + + /* RedisBloom */ + 'BF.ADD' => $prefixFirst, + 'BF.EXISTS' => $prefixFirst, + 'BF.INFO' => $prefixFirst, + 'BF.INSERT' => $prefixFirst, + 'BF.LOADCHUNK' => $prefixFirst, + 'BF.MADD' => $prefixFirst, + 'BF.MEXISTS' => $prefixFirst, + 'BF.RESERVE' => $prefixFirst, + 'BF.SCANDUMP' => $prefixFirst, + 'CF.ADD' => $prefixFirst, + 'CF.ADDNX' => $prefixFirst, + 'CF.COUNT' => $prefixFirst, + 'CF.DEL' => $prefixFirst, + 'CF.EXISTS' => $prefixFirst, + 'CF.INFO' => $prefixFirst, + 'CF.INSERT' => $prefixFirst, + 'CF.INSERTNX' => $prefixFirst, + 'CF.LOADCHUNK' => $prefixFirst, + 'CF.MEXISTS' => $prefixFirst, + 'CF.RESERVE' => $prefixFirst, + 'CF.SCANDUMP' => $prefixFirst, + 'CMS.INCRBY' => $prefixFirst, + 'CMS.INFO' => $prefixFirst, + 'CMS.INITBYDIM' => $prefixFirst, + 'CMS.INITBYPROB' => $prefixFirst, + 'CMS.QUERY' => $prefixFirst, + 'TDIGEST.ADD' => $prefixFirst, + 'TDIGEST.BYRANK' => $prefixFirst, + 'TDIGEST.BYREVRANK' => $prefixFirst, + 'TDIGEST.CDF' => $prefixFirst, + 'TDIGEST.CREATE' => $prefixFirst, + 'TDIGEST.INFO' => $prefixFirst, + 'TDIGEST.MAX' => $prefixFirst, + 'TDIGEST.MIN' => $prefixFirst, + 'TDIGEST.QUANTILE' => $prefixFirst, + 'TDIGEST.RANK' => $prefixFirst, + 'TDIGEST.RESET' => $prefixFirst, + 'TDIGEST.REVRANK' => $prefixFirst, + 'TDIGEST.TRIMMED_MEAN' => $prefixFirst, + 'TOPK.ADD' => $prefixFirst, + 'TOPK.INCRBY' => $prefixFirst, + 'TOPK.INFO' => $prefixFirst, + 'TOPK.LIST' => $prefixFirst, + 'TOPK.QUERY' => $prefixFirst, + 'TOPK.RESERVE' => $prefixFirst, + + /* RediSearch */ + 'FT.AGGREGATE' => $prefixFirst, + 'FT.ALTER' => $prefixFirst, + 'FT.CREATE' => $prefixFirst, + 'FT.CURSOR DEL' => $prefixFirst, + 'FT.CURSOR READ' => $prefixFirst, + 'FT.DROPINDEX' => $prefixFirst, + 'FT.EXPLAIN' => $prefixFirst, + 'FT.INFO' => $prefixFirst, + 'FT.PROFILE' => $prefixFirst, + 'FT.SEARCH' => $prefixFirst, + 'FT.SPELLCHECK' => $prefixFirst, + 'FT.SYNDUMP' => $prefixFirst, + 'FT.SYNUPDATE' => $prefixFirst, + 'FT.TAGVALS' => $prefixFirst, + + /* Redis TimeSeries */ + 'TS.ADD' => $prefixFirst, + 'TS.ALTER' => $prefixFirst, + 'TS.CREATE' => $prefixFirst, + 'TS.DECRBY' => $prefixFirst, + 'TS.DEL' => $prefixFirst, + 'TS.GET' => $prefixFirst, + 'TS.INCRBY' => $prefixFirst, + 'TS.INFO' => $prefixFirst, + 'TS.MGET' => $prefixFirst, + 'TS.MRANGE' => $prefixFirst, + 'TS.MREVRANGE' => $prefixFirst, + 'TS.QUERYINDEX' => $prefixFirst, + 'TS.RANGE' => $prefixFirst, + 'TS.REVRANGE' => $prefixFirst, + ]; + } + + /** + * Sets a prefix that is applied to all the keys. + * + * @param string $prefix Prefix for the keys. + */ + public function setPrefix($prefix) + { + $this->prefix = $prefix; + } + + /** + * Gets the current prefix. + * + * @return string + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * {@inheritdoc} + */ + public function process(CommandInterface $command) + { + if ($command instanceof PrefixableCommandInterface) { + $command->prefixKeys($this->prefix); + } elseif (isset($this->commands[$commandID = strtoupper($command->getId())])) { + $this->commands[$commandID]($command, $this->prefix); + } + } + + /** + * Sets an handler for the specified command ID. + * + * The callback signature must have 2 parameters of the following types: + * + * - Predis\Command\CommandInterface (command instance) + * - String (prefix) + * + * When the callback argument is omitted or NULL, the previously + * associated handler for the specified command ID is removed. + * + * @param string $commandID The ID of the command to be handled. + * @param mixed $callback A valid callable object or NULL. + * + * @throws InvalidArgumentException + */ + public function setCommandHandler($commandID, $callback = null) + { + $commandID = strtoupper($commandID); + + if (!isset($callback)) { + unset($this->commands[$commandID]); + + return; + } + + if (!is_callable($callback)) { + throw new InvalidArgumentException( + 'Callback must be a valid callable object or NULL' + ); + } + + $this->commands[$commandID] = $callback; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->getPrefix(); + } + + /** + * Applies the specified prefix only the first argument. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function first(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + $arguments[0] = "$prefix{$arguments[0]}"; + $command->setRawArguments($arguments); + } + } + + /** + * Applies the specified prefix to all the arguments. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function all(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + foreach ($arguments as &$key) { + $key = "$prefix$key"; + } + + $command->setRawArguments($arguments); + } + } + + /** + * Applies the specified prefix only to even arguments in the list. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function interleaved(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + $length = count($arguments); + + for ($i = 0; $i < $length; $i += 2) { + $arguments[$i] = "$prefix{$arguments[$i]}"; + } + + $command->setRawArguments($arguments); + } + } + + /** + * Applies the specified prefix to all the arguments but the first one. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function skipFirst(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + $length = count($arguments); + + for ($i = 1; $i < $length; ++$i) { + $arguments[$i] = "$prefix{$arguments[$i]}"; + } + + $command->setRawArguments($arguments); + } + } + + /** + * Applies the specified prefix to all the arguments but the last one. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function skipLast(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + $length = count($arguments); + + for ($i = 0; $i < $length - 1; ++$i) { + $arguments[$i] = "$prefix{$arguments[$i]}"; + } + + $command->setRawArguments($arguments); + } + } + + /** + * Applies the specified prefix to the keys of a SORT command. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function sort(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + $arguments[0] = "$prefix{$arguments[0]}"; + + if (($count = count($arguments)) > 1) { + for ($i = 1; $i < $count; ++$i) { + switch (strtoupper($arguments[$i])) { + case 'BY': + case 'STORE': + $arguments[$i] = "$prefix{$arguments[++$i]}"; + break; + + case 'GET': + $value = $arguments[++$i]; + if ($value !== '#') { + $arguments[$i] = "$prefix$value"; + } + break; + + case 'LIMIT': + $i += 2; + break; + } + } + } + + $command->setRawArguments($arguments); + } + } + + /** + * Applies the specified prefix to the keys of an EVAL-based command. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function evalKeys(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + for ($i = 2; $i < $arguments[1] + 2; ++$i) { + $arguments[$i] = "$prefix{$arguments[$i]}"; + } + + $command->setRawArguments($arguments); + } + } + + /** + * Applies the specified prefix to the keys of Z[INTERSECTION|UNION]STORE. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function zsetStore(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + $arguments[0] = "$prefix{$arguments[0]}"; + $length = ((int) $arguments[1]) + 2; + + for ($i = 2; $i < $length; ++$i) { + $arguments[$i] = "$prefix{$arguments[$i]}"; + } + + $command->setRawArguments($arguments); + } + } + + /** + * Applies the specified prefix to the key of a MIGRATE command. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function migrate(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + $arguments[2] = "$prefix{$arguments[2]}"; + $command->setRawArguments($arguments); + } + } + + /** + * Applies the specified prefix to the key of a GEORADIUS command. + * + * @param CommandInterface $command Command instance. + * @param string $prefix Prefix string. + */ + public static function georadius(CommandInterface $command, $prefix) + { + if ($arguments = $command->getArguments()) { + $arguments[0] = "$prefix{$arguments[0]}"; + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; + + if (($count = count($arguments)) > $startIndex) { + for ($i = $startIndex; $i < $count; ++$i) { + switch (strtoupper($arguments[$i])) { + case 'STORE': + case 'STOREDIST': + $arguments[$i] = "$prefix{$arguments[++$i]}"; + break; + } + } + } + + $command->setRawArguments($arguments); + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorChain.php b/plugins/cache-redis/Predis/Command/Processor/ProcessorChain.php similarity index 69% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorChain.php rename to plugins/cache-redis/Predis/Command/Processor/ProcessorChain.php index 0a4768b0a..1ce915e2b 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorChain.php +++ b/plugins/cache-redis/Predis/Command/Processor/ProcessorChain.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,21 +12,24 @@ namespace Predis\Command\Processor; +use ArrayAccess; +use ArrayIterator; +use InvalidArgumentException; use Predis\Command\CommandInterface; +use ReturnTypeWillChange; +use Traversable; /** * Default implementation of a command processors chain. - * - * @author Daniele Alessandri */ -class ProcessorChain implements \ArrayAccess, ProcessorInterface +class ProcessorChain implements ArrayAccess, ProcessorInterface { - private $processors = array(); + private $processors = []; /** * @param array $processors List of instances of ProcessorInterface. */ - public function __construct($processors = array()) + public function __construct($processors = []) { foreach ($processors as $processor) { $this->add($processor); @@ -71,11 +75,11 @@ class ProcessorChain implements \ArrayAccess, ProcessorInterface /** * Returns an iterator over the list of command processor in the chain. * - * @return \ArrayIterator + * @return Traversable */ public function getIterator() { - return new \ArrayIterator($this->processors); + return new ArrayIterator($this->processors); } /** @@ -89,30 +93,36 @@ class ProcessorChain implements \ArrayAccess, ProcessorInterface } /** - * {@inheritdoc} + * @param int $index + * @return bool */ + #[ReturnTypeWillChange] public function offsetExists($index) { return isset($this->processors[$index]); } /** - * {@inheritdoc} + * @param int $index + * @return ProcessorInterface */ + #[ReturnTypeWillChange] public function offsetGet($index) { return $this->processors[$index]; } /** - * {@inheritdoc} + * @param int $index + * @param ProcessorInterface $processor + * @return void */ + #[ReturnTypeWillChange] public function offsetSet($index, $processor) { if (!$processor instanceof ProcessorInterface) { - throw new \InvalidArgumentException( - 'A processor chain accepts only instances of '. - "'Predis\Command\Processor\ProcessorInterface'." + throw new InvalidArgumentException( + 'Processor chain accepts only instances of `Predis\Command\Processor\ProcessorInterface`' ); } @@ -120,8 +130,10 @@ class ProcessorChain implements \ArrayAccess, ProcessorInterface } /** - * {@inheritdoc} + * @param int $index + * @return void */ + #[ReturnTypeWillChange] public function offsetUnset($index) { unset($this->processors[$index]); diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorInterface.php b/plugins/cache-redis/Predis/Command/Processor/ProcessorInterface.php similarity index 84% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorInterface.php rename to plugins/cache-redis/Predis/Command/Processor/ProcessorInterface.php index 2f9105802..f915b9eb6 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/Processor/ProcessorInterface.php +++ b/plugins/cache-redis/Predis/Command/Processor/ProcessorInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,8 +16,6 @@ use Predis\Command\CommandInterface; /** * A command processor processes Redis commands before they are sent to Redis. - * - * @author Daniele Alessandri */ interface ProcessorInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/RawCommand.php b/plugins/cache-redis/Predis/Command/RawCommand.php similarity index 55% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/RawCommand.php rename to plugins/cache-redis/Predis/Command/RawCommand.php index 2dd48ca17..61b223112 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/RawCommand.php +++ b/plugins/cache-redis/Predis/Command/RawCommand.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,52 +13,45 @@ namespace Predis\Command; /** - * Class for generic "anonymous" Redis commands. + * Class representing a generic Redis command. * - * This command class does not filter input arguments or parse responses, but - * can be used to leverage the standard Predis API to execute any command simply - * by providing the needed arguments following the command signature as defined - * by Redis in its documentation. + * Arguments and responses for these commands are not normalized and they follow + * what is defined by the Redis documentation. * - * @author Daniele Alessandri + * Raw commands can be useful when implementing higher level abstractions on top + * of Predis\Client or managing internals like Redis Sentinel or Cluster as they + * are not potentially subject to hijacking from third party libraries when they + * override command handlers for standard Redis commands. */ -class RawCommand implements CommandInterface +final class RawCommand implements CommandInterface { private $slot; private $commandID; private $arguments; /** - * @param array $arguments Command ID and its arguments. - * - * @throws \InvalidArgumentException + * @param string $commandID Command ID + * @param array $arguments Command arguments */ - public function __construct(array $arguments) + public function __construct($commandID, array $arguments = []) { - if (!$arguments) { - throw new \InvalidArgumentException( - 'The arguments array must contain at least the command ID.' - ); - } - - $this->commandID = strtoupper(array_shift($arguments)); - $this->arguments = $arguments; + $this->commandID = strtoupper($commandID); + $this->setArguments($arguments); } /** * Creates a new raw command using a variadic method. * - * @param string $commandID Redis command ID. - * @param string ... Arguments list for the command. + * @param string $commandID Redis command ID + * @param string ...$args Arguments list for the command * * @return CommandInterface */ - public static function create($commandID /* [ $arg, ... */) + public static function create($commandID, ...$args) { $arguments = func_get_args(); - $command = new self($arguments); - return $command; + return new static(array_shift($arguments), $arguments); } /** @@ -116,9 +110,7 @@ class RawCommand implements CommandInterface */ public function getSlot() { - if (isset($this->slot)) { - return $this->slot; - } + return $this->slot ?? null; } /** diff --git a/plugins/cache-redis/Predis/Command/RawFactory.php b/plugins/cache-redis/Predis/Command/RawFactory.php new file mode 100644 index 000000000..2c0637f22 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/RawFactory.php @@ -0,0 +1,43 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/append - * - * @author Daniele Alessandri + * @see http://redis.io/commands/append */ -class StringAppend extends Command +class APPEND extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionAuth.php b/plugins/cache-redis/Predis/Command/Redis/AUTH.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionAuth.php rename to plugins/cache-redis/Predis/Command/Redis/AUTH.php index c8c9dedce..cc2020e5f 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionAuth.php +++ b/plugins/cache-redis/Predis/Command/Redis/AUTH.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/auth - * - * @author Daniele Alessandri + * @see http://redis.io/commands/auth */ -class ConnectionAuth extends Command +class AUTH extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/AbstractCommand/BZPOPBase.php b/plugins/cache-redis/Predis/Command/Redis/AbstractCommand/BZPOPBase.php new file mode 100644 index 000000000..ed4a53b80 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/AbstractCommand/BZPOPBase.php @@ -0,0 +1,43 @@ +setKeys($arguments, false); + } + + public function parseResponse($data) + { + $key = array_shift($data); + + if (null === $key) { + return [$key]; + } + + return array_combine([$key], [[$data[0] => $data[1]]]); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundRewriteAOF.php b/plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php similarity index 67% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundRewriteAOF.php rename to plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php index c66a294e5..97d443d90 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundRewriteAOF.php +++ b/plugins/cache-redis/Predis/Command/Redis/BGREWRITEAOF.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/bgrewriteaof - * - * @author Daniele Alessandri + * @see http://redis.io/commands/bgrewriteaof */ -class ServerBackgroundRewriteAOF extends Command +class BGREWRITEAOF extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundSave.php b/plugins/cache-redis/Predis/Command/Redis/BGSAVE.php similarity index 68% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundSave.php rename to plugins/cache-redis/Predis/Command/Redis/BGSAVE.php index 4bf67ef30..9be85773f 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerBackgroundSave.php +++ b/plugins/cache-redis/Predis/Command/Redis/BGSAVE.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/bgsave - * - * @author Daniele Alessandri + * @see http://redis.io/commands/bgsave */ -class ServerBackgroundSave extends Command +class BGSAVE extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/BITCOUNT.php b/plugins/cache-redis/Predis/Command/Redis/BITCOUNT.php new file mode 100644 index 000000000..859daf4a6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BITCOUNT.php @@ -0,0 +1,34 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/bitop - * - * @author Daniele Alessandri + * @see http://redis.io/commands/bitop */ -class StringBitOp extends Command +class BITOP extends RedisCommand { /** * {@inheritdoc} @@ -29,14 +30,14 @@ class StringBitOp extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 3 && is_array($arguments[2])) { - list($operation, $destination) = $arguments; + [$operation, $destination] = $arguments; $arguments = $arguments[2]; array_unshift($arguments, $operation, $destination); } - return $arguments; + parent::setArguments($arguments); } } diff --git a/plugins/cache-redis/Predis/Command/Redis/BITPOS.php b/plugins/cache-redis/Predis/Command/Redis/BITPOS.php new file mode 100644 index 000000000..6ea418817 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BITPOS.php @@ -0,0 +1,34 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/blpop - * - * @author Daniele Alessandri + * @see http://redis.io/commands/blpop */ -class ListPopFirstBlocking extends Command +class BLPOP extends RedisCommand { /** * {@inheritdoc} @@ -29,13 +30,13 @@ class ListPopFirstBlocking extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 2 && is_array($arguments[0])) { - list($arguments, $timeout) = $arguments; + [$arguments, $timeout] = $arguments; array_push($arguments, $timeout); } - return $arguments; + parent::setArguments($arguments); } } diff --git a/plugins/cache-redis/Predis/Command/Redis/BRPOP.php b/plugins/cache-redis/Predis/Command/Redis/BRPOP.php new file mode 100644 index 000000000..cf88dc906 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BRPOP.php @@ -0,0 +1,42 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/brpoplpush - * - * @author Daniele Alessandri + * @see http://redis.io/commands/brpoplpush */ -class ListPopLastPushHeadBlocking extends Command +class BRPOPLPUSH extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/BZMPOP.php b/plugins/cache-redis/Predis/Command/Redis/BZMPOP.php new file mode 100644 index 000000000..2ed6f7893 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BZMPOP.php @@ -0,0 +1,30 @@ + 'CAPACITY', + 'size' => 'SIZE', + 'filters' => 'FILTERS', + 'items' => 'ITEMS', + 'expansion' => 'EXPANSION', + ]; + + public function getId() + { + return 'BF.INFO'; + } + + public function setArguments(array $arguments) + { + if (isset($arguments[1])) { + $modifier = array_pop($arguments); + + if ($modifier === '') { + parent::setArguments($arguments); + + return; + } + + if (!in_array(strtoupper($modifier), $this->modifierEnum)) { + $enumValues = implode(', ', array_keys($this->modifierEnum)); + throw new UnexpectedValueException("Argument accepts only: {$enumValues} values"); + } + + $arguments[] = $this->modifierEnum[strtolower($modifier)]; + } + + parent::setArguments($arguments); + } + + public function parseResponse($data) + { + if (count($data) > 1) { + $result = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) { + if (array_key_exists($i + 1, $data)) { + $result[(string) $data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINSERT.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINSERT.php new file mode 100644 index 000000000..50e23eeaf --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFINSERT.php @@ -0,0 +1,72 @@ +setNoCreate($arguments); + $arguments = $this->getArguments(); + + if (array_key_exists(5, $arguments) && $arguments[5]) { + $arguments[5] = 'NONSCALING'; + } + + $this->setItems($arguments); + $arguments = $this->getArguments(); + + $this->setExpansion($arguments); + $arguments = $this->getArguments(); + + $this->setErrorRate($arguments); + $arguments = $this->getArguments(); + + $this->setCapacity($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFLOADCHUNK.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFLOADCHUNK.php new file mode 100644 index 000000000..76a78e997 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFLOADCHUNK.php @@ -0,0 +1,28 @@ +setExpansion($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFSCANDUMP.php b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFSCANDUMP.php new file mode 100644 index 000000000..f8576d2d0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/BloomFilter/BFSCANDUMP.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/client-list - * @link http://redis.io/commands/client-kill - * @link http://redis.io/commands/client-getname - * @link http://redis.io/commands/client-setname - * - * @author Daniele Alessandri + * @see http://redis.io/commands/client-list + * @see http://redis.io/commands/client-kill + * @see http://redis.io/commands/client-getname + * @see http://redis.io/commands/client-setname */ -class ServerClient extends Command +class CLIENT extends RedisCommand { /** * {@inheritdoc} @@ -44,7 +45,7 @@ class ServerClient extends Command case 'SETNAME': default: return $data; - } + } // @codeCoverageIgnore } /** @@ -56,13 +57,13 @@ class ServerClient extends Command */ protected function parseClientList($data) { - $clients = array(); + $clients = []; foreach (explode("\n", $data, -1) as $clientData) { - $client = array(); + $client = []; foreach (explode(' ', $clientData) as $kv) { - @list($k, $v) = explode('=', $kv); + @[$k, $v] = explode('=', $kv); $client[$k] = $v; } diff --git a/plugins/cache-redis/Predis/Command/Redis/CLUSTER.php b/plugins/cache-redis/Predis/Command/Redis/CLUSTER.php new file mode 100644 index 000000000..3bbb77cc8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CLUSTER.php @@ -0,0 +1,26 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/config-set - * @link http://redis.io/commands/config-get - * @link http://redis.io/commands/config-resetstat - * @link http://redis.io/commands/config-rewrite - * - * @author Daniele Alessandri + * @see http://redis.io/commands/config-set + * @see http://redis.io/commands/config-get + * @see http://redis.io/commands/config-resetstat + * @see http://redis.io/commands/config-rewrite */ -class ServerConfig extends Command +class CONFIG extends RedisCommand { /** * {@inheritdoc} @@ -35,7 +36,11 @@ class ServerConfig extends Command public function parseResponse($data) { if (is_array($data)) { - $result = array(); + if ($data !== array_values($data)) { + return $data; // Relay + } + + $result = []; for ($i = 0; $i < count($data); ++$i) { $result[$data[$i]] = $data[++$i]; diff --git a/plugins/cache-redis/Predis/Command/Redis/COPY.php b/plugins/cache-redis/Predis/Command/Redis/COPY.php new file mode 100644 index 000000000..cb6ec659b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/COPY.php @@ -0,0 +1,47 @@ +setDB($arguments); + $arguments = $this->getArguments(); + + $this->setReplace($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/ACL.php b/plugins/cache-redis/Predis/Command/Redis/Container/ACL.php new file mode 100644 index 000000000..2699d37e8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/ACL.php @@ -0,0 +1,28 @@ +client = $client; + } + + /** + * {@inheritDoc} + */ + public function __call(string $subcommandID, array $arguments) + { + array_unshift($arguments, strtoupper($subcommandID)); + + return $this->client->executeCommand( + $this->client->createCommand($this->getContainerCommandId(), $arguments) + ); + } + + abstract public function getContainerCommandId(): string; +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/CLUSTER.php b/plugins/cache-redis/Predis/Command/Redis/Container/CLUSTER.php new file mode 100644 index 000000000..b2925b19b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/CLUSTER.php @@ -0,0 +1,29 @@ + FunctionContainer::class, + ]; + + /** + * Creates container command. + * + * @param ClientInterface $client + * @param string $containerCommandID + * @return ContainerInterface + */ + public static function create(ClientInterface $client, string $containerCommandID): ContainerInterface + { + $containerCommandID = strtoupper($containerCommandID); + $commandModule = self::resolveCommandModuleByPrefix($containerCommandID); + + if (null !== $commandModule) { + if (class_exists($containerClass = self::CONTAINER_NAMESPACE . '\\' . $commandModule . '\\' . $containerCommandID)) { + return new $containerClass($client); + } + + throw new UnexpectedValueException('Given module container command is not supported.'); + } + + if (class_exists($containerClass = self::CONTAINER_NAMESPACE . '\\' . $containerCommandID)) { + return new $containerClass($client); + } + + if (array_key_exists($containerCommandID, self::$specialMappings)) { + $containerClass = self::$specialMappings[$containerCommandID]; + + return new $containerClass($client); + } + + throw new UnexpectedValueException('Given container command is not supported.'); + } + + /** + * @param string $commandID + * @return string|null + */ + private static function resolveCommandModuleByPrefix(string $commandID): ?string + { + $modules = ClientConfiguration::getModules(); + + foreach ($modules as $module) { + if (preg_match("/^{$module['commandPrefix']}/", $commandID)) { + return $module['name']; + } + } + + return null; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Container/ContainerInterface.php b/plugins/cache-redis/Predis/Command/Redis/Container/ContainerInterface.php new file mode 100644 index 000000000..e30c539e3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Container/ContainerInterface.php @@ -0,0 +1,33 @@ + 1) { + $result = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) { + if (array_key_exists($i + 1, $data)) { + $result[(string) $data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php new file mode 100644 index 000000000..8f1f23ff3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM.php @@ -0,0 +1,28 @@ + 1) { + $result = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) { + if (array_key_exists($i + 1, $data)) { + $result[(string) $data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php new file mode 100644 index 000000000..3a49a38d7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERT.php @@ -0,0 +1,52 @@ +setNoCreate($arguments); + $arguments = $this->getArguments(); + + $this->setItems($arguments); + $arguments = $this->getArguments(); + + $this->setCapacity($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERTNX.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERTNX.php new file mode 100644 index 000000000..629327b4f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFINSERTNX.php @@ -0,0 +1,27 @@ +setExpansion($arguments); + $arguments = $this->getArguments(); + + $this->setMaxIterations($arguments); + $arguments = $this->getArguments(); + + $this->setBucketSize($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFSCANDUMP.php b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFSCANDUMP.php new file mode 100644 index 000000000..59caa651c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/CuckooFilter/CFSCANDUMP.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/dbsize - * - * @author Daniele Alessandri + * @see http://redis.io/commands/dbsize */ -class ServerDatabaseSize extends Command +class DBSIZE extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrement.php b/plugins/cache-redis/Predis/Command/Redis/DECR.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrement.php rename to plugins/cache-redis/Predis/Command/Redis/DECR.php index aa5808cd0..453f683a8 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrement.php +++ b/plugins/cache-redis/Predis/Command/Redis/DECR.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/decr - * - * @author Daniele Alessandri + * @see http://redis.io/commands/decr */ -class StringDecrement extends Command +class DECR extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrementBy.php b/plugins/cache-redis/Predis/Command/Redis/DECRBY.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrementBy.php rename to plugins/cache-redis/Predis/Command/Redis/DECRBY.php index cbf3e1124..15eace6be 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringDecrementBy.php +++ b/plugins/cache-redis/Predis/Command/Redis/DECRBY.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/decrby - * - * @author Daniele Alessandri + * @see http://redis.io/commands/decrby */ -class StringDecrementBy extends Command +class DECRBY extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/DEL.php b/plugins/cache-redis/Predis/Command/Redis/DEL.php new file mode 100644 index 000000000..359bbf4ff --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/DEL.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/dump - * - * @author Daniele Alessandri + * @see http://redis.io/commands/dump */ -class KeyDump extends Command +class DUMP extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionEcho.php b/plugins/cache-redis/Predis/Command/Redis/ECHO_.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionEcho.php rename to plugins/cache-redis/Predis/Command/Redis/ECHO_.php index fd4960971..c04adea33 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ConnectionEcho.php +++ b/plugins/cache-redis/Predis/Command/Redis/ECHO_.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/echo - * - * @author Daniele Alessandri + * @see http://redis.io/commands/echo */ -class ConnectionEcho extends Command +class ECHO_ extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerEvalSHA.php b/plugins/cache-redis/Predis/Command/Redis/EVALSHA.php similarity index 70% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerEvalSHA.php rename to plugins/cache-redis/Predis/Command/Redis/EVALSHA.php index 520a8e985..ee7a38087 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerEvalSHA.php +++ b/plugins/cache-redis/Predis/Command/Redis/EVALSHA.php @@ -3,20 +3,19 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; /** - * @link http://redis.io/commands/evalsha - * - * @author Daniele Alessandri + * @see http://redis.io/commands/evalsha */ -class ServerEvalSHA extends ServerEval +class EVALSHA extends EVAL_ { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/EVALSHA_RO.php b/plugins/cache-redis/Predis/Command/Redis/EVALSHA_RO.php new file mode 100644 index 000000000..809a08757 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EVALSHA_RO.php @@ -0,0 +1,27 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/eval - * - * @author Daniele Alessandri + * @see http://redis.io/commands/eval */ -class ServerEval extends Command +class EVAL_ extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/EVAL_RO.php b/plugins/cache-redis/Predis/Command/Redis/EVAL_RO.php new file mode 100644 index 000000000..cef8bd35e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EVAL_RO.php @@ -0,0 +1,34 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/exec - * - * @author Daniele Alessandri + * @see http://redis.io/commands/exec */ -class TransactionExec extends Command +class EXEC extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/EXISTS.php b/plugins/cache-redis/Predis/Command/Redis/EXISTS.php new file mode 100644 index 000000000..8731c9d84 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/EXISTS.php @@ -0,0 +1,29 @@ +setTimeout($arguments); + $arguments = $this->getArguments(); + + $this->setTo($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/FCALL.php b/plugins/cache-redis/Predis/Command/Redis/FCALL.php new file mode 100644 index 000000000..cc5639321 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/FCALL.php @@ -0,0 +1,33 @@ + 2) { + for ($i = 2, $iMax = count($arguments); $i < $iMax; $i++) { + $processedArguments[] = $arguments[$i]; + } + } + + parent::setArguments($processedArguments); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushAll.php b/plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushAll.php rename to plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php index c35b2ad6a..a03a3133d 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushAll.php +++ b/plugins/cache-redis/Predis/Command/Redis/FLUSHALL.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/flushall - * - * @author Daniele Alessandri + * @see http://redis.io/commands/flushall */ -class ServerFlushAll extends Command +class FLUSHALL extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushDatabase.php b/plugins/cache-redis/Predis/Command/Redis/FLUSHDB.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushDatabase.php rename to plugins/cache-redis/Predis/Command/Redis/FLUSHDB.php index 3da6b320d..67a2d485b 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerFlushDatabase.php +++ b/plugins/cache-redis/Predis/Command/Redis/FLUSHDB.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/flushdb - * - * @author Daniele Alessandri + * @see http://redis.io/commands/flushdb */ -class ServerFlushDatabase extends Command +class FLUSHDB extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/FUNCTIONS.php b/plugins/cache-redis/Predis/Command/Redis/FUNCTIONS.php new file mode 100644 index 000000000..7f4fde79a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/FUNCTIONS.php @@ -0,0 +1,50 @@ +strategyResolver = new SubcommandStrategyResolver(); + } + + public function getId() + { + return 'FUNCTION'; + } + + public function setArguments(array $arguments) + { + $strategy = $this->strategyResolver->resolve('functions', strtolower($arguments[0])); + $arguments = $strategy->processArguments($arguments); + + parent::setArguments($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GEOADD.php b/plugins/cache-redis/Predis/Command/Redis/GEOADD.php new file mode 100644 index 000000000..56156b72b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEOADD.php @@ -0,0 +1,43 @@ +setSorting($arguments); + $arguments = $this->getArguments(); + + $this->setWithCoord($arguments); + $arguments = $this->getArguments(); + + $this->setWithDist($arguments); + $arguments = $this->getArguments(); + + $this->setWithHash($arguments); + $arguments = $this->getArguments(); + + $this->setCount($arguments, $arguments[5] ?? false); + $arguments = $this->getArguments(); + + $this->setFrom($arguments); + $arguments = $this->getArguments(); + + $this->setBy($arguments); + $this->filterArguments(); + } + + public function parseResponse($data) + { + $parsedData = []; + $itemKey = ''; + + foreach ($data as $item) { + if (!is_array($item)) { + $parsedData[] = $item; + continue; + } + + foreach ($item as $key => $itemRow) { + if ($key === 0) { + $itemKey = $itemRow; + continue; + } + + if (is_string($itemRow)) { + $parsedData[$itemKey]['dist'] = round((float) $itemRow, 5); + } elseif (is_int($itemRow)) { + $parsedData[$itemKey]['hash'] = $itemRow; + } else { + $parsedData[$itemKey]['lng'] = round($itemRow[0], 5); + $parsedData[$itemKey]['lat'] = round($itemRow[1], 5); + } + } + } + + return $parsedData; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/GEOSEARCHSTORE.php b/plugins/cache-redis/Predis/Command/Redis/GEOSEARCHSTORE.php new file mode 100644 index 000000000..6798db7ed --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GEOSEARCHSTORE.php @@ -0,0 +1,71 @@ +setStoreDist($arguments); + $arguments = $this->getArguments(); + + $this->setCount($arguments, $arguments[6] ?? false); + $arguments = $this->getArguments(); + + $this->setSorting($arguments); + $arguments = $this->getArguments(); + + $this->setFrom($arguments); + $arguments = $this->getArguments(); + + $this->setBy($arguments); + $this->filterArguments(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGet.php b/plugins/cache-redis/Predis/Command/Redis/GET.php similarity index 59% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringGet.php rename to plugins/cache-redis/Predis/Command/Redis/GET.php index 138e915c2..e9177ab28 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGet.php +++ b/plugins/cache-redis/Predis/Command/Redis/GET.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/get - * - * @author Daniele Alessandri + * @see http://redis.io/commands/get */ -class StringGet extends Command +class GET extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetBit.php b/plugins/cache-redis/Predis/Command/Redis/GETBIT.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetBit.php rename to plugins/cache-redis/Predis/Command/Redis/GETBIT.php index 3c5b4f9b7..00ee97085 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetBit.php +++ b/plugins/cache-redis/Predis/Command/Redis/GETBIT.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/getbit - * - * @author Daniele Alessandri + * @see http://redis.io/commands/getbit */ -class StringGetBit extends Command +class GETBIT extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/GETDEL.php b/plugins/cache-redis/Predis/Command/Redis/GETDEL.php new file mode 100644 index 000000000..7e4df93c6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/GETDEL.php @@ -0,0 +1,23 @@ + 'EX', + 'px' => 'PX', + 'exat' => 'EXAT', + 'pxat' => 'PXAT', + 'persist' => 'PERSIST', + ]; + + public function getId() + { + return 'GETEX'; + } + + public function setArguments(array $arguments) + { + if (!array_key_exists(1, $arguments) || $arguments[1] === '') { + parent::setArguments([$arguments[0]]); + + return; + } + + if (!in_array(strtoupper($arguments[1]), self::$modifierEnum)) { + $enumValues = implode(', ', array_keys(self::$modifierEnum)); + throw new UnexpectedValueException("Modifier argument accepts only: {$enumValues} values"); + } + + if ($arguments[1] === 'persist') { + parent::setArguments([$arguments[0], self::$modifierEnum[$arguments[1]]]); + + return; + } + + $arguments[1] = self::$modifierEnum[$arguments[1]]; + + if (!array_key_exists(2, $arguments)) { + throw new UnexpectedValueException('You should provide value for current modifier'); + } + + parent::setArguments($arguments); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetRange.php b/plugins/cache-redis/Predis/Command/Redis/GETRANGE.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetRange.php rename to plugins/cache-redis/Predis/Command/Redis/GETRANGE.php index bb10565b5..c6feb408b 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetRange.php +++ b/plugins/cache-redis/Predis/Command/Redis/GETRANGE.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/getrange - * - * @author Daniele Alessandri + * @see http://redis.io/commands/getrange */ -class StringGetRange extends Command +class GETRANGE extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetSet.php b/plugins/cache-redis/Predis/Command/Redis/GETSET.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetSet.php rename to plugins/cache-redis/Predis/Command/Redis/GETSET.php index b68870d4c..37a38619c 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringGetSet.php +++ b/plugins/cache-redis/Predis/Command/Redis/GETSET.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/getset - * - * @author Daniele Alessandri + * @see http://redis.io/commands/getset */ -class StringGetSet extends Command +class GETSET extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/HDEL.php b/plugins/cache-redis/Predis/Command/Redis/HDEL.php new file mode 100644 index 000000000..bb1fba345 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HDEL.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/hget - * - * @author Daniele Alessandri + * @see http://redis.io/commands/hget */ -class HashGet extends Command +class HGET extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashGetAll.php b/plugins/cache-redis/Predis/Command/Redis/HGETALL.php similarity index 61% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashGetAll.php rename to plugins/cache-redis/Predis/Command/Redis/HGETALL.php index d69867521..c5f566bae 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashGetAll.php +++ b/plugins/cache-redis/Predis/Command/Redis/HGETALL.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/hgetall - * - * @author Daniele Alessandri + * @see http://redis.io/commands/hgetall */ -class HashGetAll extends Command +class HGETALL extends RedisCommand { /** * {@inheritdoc} @@ -31,7 +32,11 @@ class HashGetAll extends Command */ public function parseResponse($data) { - $result = array(); + if ($data !== array_values($data)) { + return $data; // Relay + } + + $result = []; for ($i = 0; $i < count($data); ++$i) { $result[$data[$i]] = $data[++$i]; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementBy.php b/plugins/cache-redis/Predis/Command/Redis/HINCRBY.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementBy.php rename to plugins/cache-redis/Predis/Command/Redis/HINCRBY.php index a37359ffb..cf60ecea1 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementBy.php +++ b/plugins/cache-redis/Predis/Command/Redis/HINCRBY.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/hincrby - * - * @author Daniele Alessandri + * @see http://redis.io/commands/hincrby */ -class HashIncrementBy extends Command +class HINCRBY extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementByFloat.php b/plugins/cache-redis/Predis/Command/Redis/HINCRBYFLOAT.php similarity index 57% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementByFloat.php rename to plugins/cache-redis/Predis/Command/Redis/HINCRBYFLOAT.php index bce9714fc..566ee874a 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashIncrementByFloat.php +++ b/plugins/cache-redis/Predis/Command/Redis/HINCRBYFLOAT.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/hincrbyfloat - * - * @author Daniele Alessandri + * @see http://redis.io/commands/hincrbyfloat */ -class HashIncrementByFloat extends Command +class HINCRBYFLOAT extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashKeys.php b/plugins/cache-redis/Predis/Command/Redis/HKEYS.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashKeys.php rename to plugins/cache-redis/Predis/Command/Redis/HKEYS.php index 28266020a..43986ec3f 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashKeys.php +++ b/plugins/cache-redis/Predis/Command/Redis/HKEYS.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/hkeys - * - * @author Daniele Alessandri + * @see http://redis.io/commands/hkeys */ -class HashKeys extends Command +class HKEYS extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashLength.php b/plugins/cache-redis/Predis/Command/Redis/HLEN.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashLength.php rename to plugins/cache-redis/Predis/Command/Redis/HLEN.php index d70926f1c..32903ea62 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashLength.php +++ b/plugins/cache-redis/Predis/Command/Redis/HLEN.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/hlen - * - * @author Daniele Alessandri + * @see http://redis.io/commands/hlen */ -class HashLength extends Command +class HLEN extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/HMGET.php b/plugins/cache-redis/Predis/Command/Redis/HMGET.php new file mode 100644 index 000000000..077373f68 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HMGET.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/hmset - * - * @author Daniele Alessandri + * @see http://redis.io/commands/hmset */ -class HashSetMultiple extends Command +class HMSET extends RedisCommand { /** * {@inheritdoc} @@ -29,10 +30,10 @@ class HashSetMultiple extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 2 && is_array($arguments[1])) { - $flattenedKVs = array($arguments[0]); + $flattenedKVs = [$arguments[0]]; $args = $arguments[1]; foreach ($args as $k => $v) { @@ -40,9 +41,9 @@ class HashSetMultiple extends Command $flattenedKVs[] = $v; } - return $flattenedKVs; + $arguments = $flattenedKVs; } - return $arguments; + parent::setArguments($arguments); } } diff --git a/plugins/cache-redis/Predis/Command/Redis/HRANDFIELD.php b/plugins/cache-redis/Predis/Command/Redis/HRANDFIELD.php new file mode 100644 index 000000000..62ce7dbe9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HRANDFIELD.php @@ -0,0 +1,53 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/hscan - * - * @author Daniele Alessandri + * @see http://redis.io/commands/hscan */ -class HashScan extends Command +class HSCAN extends RedisCommand { /** * {@inheritdoc} @@ -29,14 +30,14 @@ class HashScan extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 3 && is_array($arguments[2])) { $options = $this->prepareOptions(array_pop($arguments)); $arguments = array_merge($arguments, $options); } - return $arguments; + parent::setArguments($arguments); } /** @@ -49,7 +50,7 @@ class HashScan extends Command protected function prepareOptions($options) { $options = array_change_key_case($options, CASE_UPPER); - $normalized = array(); + $normalized = []; if (!empty($options['MATCH'])) { $normalized[] = 'MATCH'; @@ -71,7 +72,7 @@ class HashScan extends Command { if (is_array($data)) { $fields = $data[1]; - $result = array(); + $result = []; for ($i = 0; $i < count($fields); ++$i) { $result[$fields[$i]] = $fields[++$i]; diff --git a/plugins/cache-redis/Predis/Command/Redis/HSET.php b/plugins/cache-redis/Predis/Command/Redis/HSET.php new file mode 100644 index 000000000..662094a3c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/HSET.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/hstrlen - * - * @author Daniele Alessandri + * @see http://redis.io/commands/hstrlen */ -class HashStringLength extends Command +class HSTRLEN extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashValues.php b/plugins/cache-redis/Predis/Command/Redis/HVALS.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/HashValues.php rename to plugins/cache-redis/Predis/Command/Redis/HVALS.php index 0a5ea5f61..71172c709 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/HashValues.php +++ b/plugins/cache-redis/Predis/Command/Redis/HVALS.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/hvals - * - * @author Daniele Alessandri + * @see http://redis.io/commands/hvals */ -class HashValues extends Command +class HVALS extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionDiscard.php b/plugins/cache-redis/Predis/Command/Redis/INCR.php similarity index 59% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionDiscard.php rename to plugins/cache-redis/Predis/Command/Redis/INCR.php index 44aca2b11..80bc1ee0e 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionDiscard.php +++ b/plugins/cache-redis/Predis/Command/Redis/INCR.php @@ -3,26 +3,27 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/discard - * - * @author Daniele Alessandri + * @see http://redis.io/commands/incr */ -class TransactionDiscard extends Command +class INCR extends RedisCommand { /** * {@inheritdoc} */ public function getId() { - return 'DISCARD'; + return 'INCR'; } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementBy.php b/plugins/cache-redis/Predis/Command/Redis/INCRBY.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementBy.php rename to plugins/cache-redis/Predis/Command/Redis/INCRBY.php index 9d8241a25..f4ce4e32c 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementBy.php +++ b/plugins/cache-redis/Predis/Command/Redis/INCRBY.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/incrby - * - * @author Daniele Alessandri + * @see http://redis.io/commands/incrby */ -class StringIncrementBy extends Command +class INCRBY extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementByFloat.php b/plugins/cache-redis/Predis/Command/Redis/INCRBYFLOAT.php similarity index 57% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementByFloat.php rename to plugins/cache-redis/Predis/Command/Redis/INCRBYFLOAT.php index 164a0869b..fb626edbe 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringIncrementByFloat.php +++ b/plugins/cache-redis/Predis/Command/Redis/INCRBYFLOAT.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/incrbyfloat - * - * @author Daniele Alessandri + * @see http://redis.io/commands/incrbyfloat */ -class StringIncrementByFloat extends Command +class INCRBYFLOAT extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/INFO.php b/plugins/cache-redis/Predis/Command/Redis/INFO.php new file mode 100644 index 000000000..26a62c2a3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/INFO.php @@ -0,0 +1,157 @@ +parseNewResponseFormat($lines); + } else { + return $this->parseOldResponseFormat($lines); + } + } + + /** + * {@inheritdoc} + */ + public function parseNewResponseFormat($lines) + { + $info = []; + $current = null; + + foreach ($lines as $row) { + if ($row === '') { + continue; + } + + if (preg_match('/^# (\w+)$/', $row, $matches)) { + $info[$matches[1]] = []; + $current = &$info[$matches[1]]; + continue; + } + + [$k, $v] = $this->parseRow($row); + $current[$k] = $v; + } + + return $info; + } + + /** + * {@inheritdoc} + */ + public function parseOldResponseFormat($lines) + { + $info = []; + + foreach ($lines as $row) { + if (strpos($row, ':') === false) { + continue; + } + + [$k, $v] = $this->parseRow($row); + $info[$k] = $v; + } + + return $info; + } + + /** + * Parses a single row of the response and returns the key-value pair. + * + * @param string $row Single row of the response. + * + * @return array + */ + protected function parseRow($row) + { + if (preg_match('/^module:name/', $row)) { + return $this->parseModuleRow($row); + } + + [$k, $v] = explode(':', $row, 2); + + if (preg_match('/^db\d+$/', $k)) { + $v = $this->parseDatabaseStats($v); + } + + return [$k, $v]; + } + + /** + * Extracts the statistics of each logical DB from the string buffer. + * + * @param string $str Response buffer. + * + * @return array + */ + protected function parseDatabaseStats($str) + { + $db = []; + + foreach (explode(',', $str) as $dbvar) { + [$dbvk, $dbvv] = explode('=', $dbvar); + $db[trim($dbvk)] = $dbvv; + } + + return $db; + } + + /** + * Parsing module rows because of different format. + * + * @param string $row + * @return array + */ + protected function parseModuleRow(string $row): array + { + [$moduleKeyword, $moduleData] = explode(':', $row); + $explodedData = explode(',', $moduleData); + $parsedData = []; + + foreach ($explodedData as $moduleDataRow) { + [$k, $v] = explode('=', $moduleDataRow); + + if ($k === 'name') { + $parsedData[0] = $v; + continue; + } + + $parsedData[1][$k] = $v; + } + + return $parsedData; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRAPPEND.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRAPPEND.php new file mode 100644 index 000000000..1b2a92036 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONARRAPPEND.php @@ -0,0 +1,28 @@ +setSpace($arguments); + $arguments = $this->getArguments(); + + $this->setNewline($arguments); + $arguments = $this->getArguments(); + + $this->setIndent($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONMERGE.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMERGE.php new file mode 100644 index 000000000..a13222833 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONMERGE.php @@ -0,0 +1,29 @@ +setSubcommand($arguments); + $this->filterArguments(); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRAPPEND.php b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRAPPEND.php new file mode 100644 index 000000000..9b11458e4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Json/JSONSTRAPPEND.php @@ -0,0 +1,28 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/keys - * - * @author Daniele Alessandri + * @see http://redis.io/commands/keys */ -class KeyKeys extends Command +class KEYS extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerLastSave.php b/plugins/cache-redis/Predis/Command/Redis/LASTSAVE.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerLastSave.php rename to plugins/cache-redis/Predis/Command/Redis/LASTSAVE.php index feeb19a8a..0672845d2 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerLastSave.php +++ b/plugins/cache-redis/Predis/Command/Redis/LASTSAVE.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/lastsave - * - * @author Daniele Alessandri + * @see http://redis.io/commands/lastsave */ -class ServerLastSave extends Command +class LASTSAVE extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/LCS.php b/plugins/cache-redis/Predis/Command/Redis/LCS.php new file mode 100644 index 000000000..e8663f457 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LCS.php @@ -0,0 +1,69 @@ +filterArguments(); + } + + public function parseResponse($data) + { + if (is_array($data)) { + if ($data !== array_values($data)) { + return $data; // Relay + } + + return [$data[0] => $data[1], $data[2] => $data[3]]; + } + + return $data; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListIndex.php b/plugins/cache-redis/Predis/Command/Redis/LINDEX.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListIndex.php rename to plugins/cache-redis/Predis/Command/Redis/LINDEX.php index 27c64be73..80510e1e4 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListIndex.php +++ b/plugins/cache-redis/Predis/Command/Redis/LINDEX.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/lindex - * - * @author Daniele Alessandri + * @see http://redis.io/commands/lindex */ -class ListIndex extends Command +class LINDEX extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListInsert.php b/plugins/cache-redis/Predis/Command/Redis/LINSERT.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListInsert.php rename to plugins/cache-redis/Predis/Command/Redis/LINSERT.php index 7d53d11b2..a81cf3414 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListInsert.php +++ b/plugins/cache-redis/Predis/Command/Redis/LINSERT.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/linsert - * - * @author Daniele Alessandri + * @see http://redis.io/commands/linsert */ -class ListInsert extends Command +class LINSERT extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListLength.php b/plugins/cache-redis/Predis/Command/Redis/LLEN.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListLength.php rename to plugins/cache-redis/Predis/Command/Redis/LLEN.php index 6495beb77..628e60280 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListLength.php +++ b/plugins/cache-redis/Predis/Command/Redis/LLEN.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/llen - * - * @author Daniele Alessandri + * @see http://redis.io/commands/llen */ -class ListLength extends Command +class LLEN extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/LMOVE.php b/plugins/cache-redis/Predis/Command/Redis/LMOVE.php new file mode 100644 index 000000000..f7bad007d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LMOVE.php @@ -0,0 +1,23 @@ +setCount($arguments); + $arguments = $this->getArguments(); + + $this->setLeftRight($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + $this->filterArguments(); + } + + public function parseResponse($data) + { + if (null === $data) { + return null; + } + + return [$data[0] => $data[1]]; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopFirst.php b/plugins/cache-redis/Predis/Command/Redis/LPOP.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopFirst.php rename to plugins/cache-redis/Predis/Command/Redis/LPOP.php index 84d5d6734..d375bacaf 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopFirst.php +++ b/plugins/cache-redis/Predis/Command/Redis/LPOP.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/lpop - * - * @author Daniele Alessandri + * @see http://redis.io/commands/lpop */ -class ListPopFirst extends Command +class LPOP extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/LPUSH.php b/plugins/cache-redis/Predis/Command/Redis/LPUSH.php new file mode 100644 index 000000000..f3e0f9e73 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/LPUSH.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/lpushx - * - * @author Daniele Alessandri + * @see http://redis.io/commands/lpushx */ -class ListPushHeadX extends Command +class LPUSHX extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListRange.php b/plugins/cache-redis/Predis/Command/Redis/LRANGE.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListRange.php rename to plugins/cache-redis/Predis/Command/Redis/LRANGE.php index 32a21a6e6..4092dae73 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListRange.php +++ b/plugins/cache-redis/Predis/Command/Redis/LRANGE.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/lrange - * - * @author Daniele Alessandri + * @see http://redis.io/commands/lrange */ -class ListRange extends Command +class LRANGE extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListRemove.php b/plugins/cache-redis/Predis/Command/Redis/LREM.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListRemove.php rename to plugins/cache-redis/Predis/Command/Redis/LREM.php index c5800899b..47e06c2fd 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListRemove.php +++ b/plugins/cache-redis/Predis/Command/Redis/LREM.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/lrem - * - * @author Daniele Alessandri + * @see http://redis.io/commands/lrem */ -class ListRemove extends Command +class LREM extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListSet.php b/plugins/cache-redis/Predis/Command/Redis/LSET.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListSet.php rename to plugins/cache-redis/Predis/Command/Redis/LSET.php index 5e59864de..255bb8976 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListSet.php +++ b/plugins/cache-redis/Predis/Command/Redis/LSET.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/lset - * - * @author Daniele Alessandri + * @see http://redis.io/commands/lset */ -class ListSet extends Command +class LSET extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListTrim.php b/plugins/cache-redis/Predis/Command/Redis/LTRIM.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListTrim.php rename to plugins/cache-redis/Predis/Command/Redis/LTRIM.php index 193141809..c958ef6ec 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListTrim.php +++ b/plugins/cache-redis/Predis/Command/Redis/LTRIM.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/ltrim - * - * @author Daniele Alessandri + * @see http://redis.io/commands/ltrim */ -class ListTrim extends Command +class LTRIM extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/MGET.php b/plugins/cache-redis/Predis/Command/Redis/MGET.php new file mode 100644 index 000000000..249cc43f8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MGET.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/migrate - * - * @author Daniele Alessandri + * @see http://redis.io/commands/migrate */ -class KeyMigrate extends Command +class MIGRATE extends RedisCommand { /** * {@inheritdoc} @@ -29,7 +30,7 @@ class KeyMigrate extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (is_array(end($arguments))) { foreach (array_pop($arguments) as $modifier => $value) { @@ -45,6 +46,6 @@ class KeyMigrate extends Command } } - return $arguments; + parent::setArguments($arguments); } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerMonitor.php b/plugins/cache-redis/Predis/Command/Redis/MONITOR.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerMonitor.php rename to plugins/cache-redis/Predis/Command/Redis/MONITOR.php index 1c3d33095..06e9e59b6 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerMonitor.php +++ b/plugins/cache-redis/Predis/Command/Redis/MONITOR.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/monitor - * - * @author Daniele Alessandri + * @see http://redis.io/commands/monitor */ -class ServerMonitor extends Command +class MONITOR extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/MOVE.php b/plugins/cache-redis/Predis/Command/Redis/MOVE.php new file mode 100644 index 000000000..cd7a8e522 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/MOVE.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/mset - * - * @author Daniele Alessandri + * @see http://redis.io/commands/mset */ -class StringSetMultiple extends Command +class MSET extends RedisCommand { /** * {@inheritdoc} @@ -29,10 +30,10 @@ class StringSetMultiple extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 1 && is_array($arguments[0])) { - $flattenedKVs = array(); + $flattenedKVs = []; $args = $arguments[0]; foreach ($args as $k => $v) { @@ -40,9 +41,9 @@ class StringSetMultiple extends Command $flattenedKVs[] = $v; } - return $flattenedKVs; + $arguments = $flattenedKVs; } - return $arguments; + parent::setArguments($arguments); } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyType.php b/plugins/cache-redis/Predis/Command/Redis/MSETNX.php similarity index 55% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/KeyType.php rename to plugins/cache-redis/Predis/Command/Redis/MSETNX.php index f4f06e451..94d9ce269 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyType.php +++ b/plugins/cache-redis/Predis/Command/Redis/MSETNX.php @@ -3,26 +3,25 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; /** - * @link http://redis.io/commands/type - * - * @author Daniele Alessandri + * @see http://redis.io/commands/msetnx */ -class KeyType extends Command +class MSETNX extends MSET { /** * {@inheritdoc} */ public function getId() { - return 'TYPE'; + return 'MSETNX'; } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionMulti.php b/plugins/cache-redis/Predis/Command/Redis/MULTI.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionMulti.php rename to plugins/cache-redis/Predis/Command/Redis/MULTI.php index 673bf55da..d8f96e64b 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/TransactionMulti.php +++ b/plugins/cache-redis/Predis/Command/Redis/MULTI.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/multi - * - * @author Daniele Alessandri + * @see http://redis.io/commands/multi */ -class TransactionMulti extends Command +class MULTI extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerObject.php b/plugins/cache-redis/Predis/Command/Redis/OBJECT_.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerObject.php rename to plugins/cache-redis/Predis/Command/Redis/OBJECT_.php index f921701c2..856a8740e 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerObject.php +++ b/plugins/cache-redis/Predis/Command/Redis/OBJECT_.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/object - * - * @author Daniele Alessandri + * @see http://redis.io/commands/object */ -class ServerObject extends Command +class OBJECT_ extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/PERSIST.php b/plugins/cache-redis/Predis/Command/Redis/PERSIST.php new file mode 100644 index 000000000..8fd70e7aa --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PERSIST.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/pexpire - * - * @author Daniele Alessandri + * @see http://redis.io/commands/pexpire */ -class KeyPreciseExpire extends KeyExpire +class PEXPIRE extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyPreciseExpireAt.php b/plugins/cache-redis/Predis/Command/Redis/PEXPIREAT.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/KeyPreciseExpireAt.php rename to plugins/cache-redis/Predis/Command/Redis/PEXPIREAT.php index e41921870..6723a71e8 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyPreciseExpireAt.php +++ b/plugins/cache-redis/Predis/Command/Redis/PEXPIREAT.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/pexpireat - * - * @author Daniele Alessandri + * @see http://redis.io/commands/pexpireat */ -class KeyPreciseExpireAt extends KeyExpireAt +class PEXPIREAT extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/PEXPIRETIME.php b/plugins/cache-redis/Predis/Command/Redis/PEXPIRETIME.php new file mode 100644 index 000000000..de1289698 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PEXPIRETIME.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/ping - * - * @author Daniele Alessandri + * @see http://redis.io/commands/ping */ -class ConnectionPing extends Command +class PING extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringPreciseSetExpire.php b/plugins/cache-redis/Predis/Command/Redis/PSETEX.php similarity index 57% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringPreciseSetExpire.php rename to plugins/cache-redis/Predis/Command/Redis/PSETEX.php index 2faa954d0..3b8131e4a 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringPreciseSetExpire.php +++ b/plugins/cache-redis/Predis/Command/Redis/PSETEX.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/psetex - * - * @author Daniele Alessandri + * @see http://redis.io/commands/psetex */ -class StringPreciseSetExpire extends StringSetExpire +class PSETEX extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/PSUBSCRIBE.php b/plugins/cache-redis/Predis/Command/Redis/PSUBSCRIBE.php new file mode 100644 index 000000000..7377c7b16 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PSUBSCRIBE.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/pttl - * - * @author Daniele Alessandri + * @see http://redis.io/commands/pttl */ -class KeyPreciseTimeToLive extends KeyTimeToLive +class PTTL extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPublish.php b/plugins/cache-redis/Predis/Command/Redis/PUBLISH.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPublish.php rename to plugins/cache-redis/Predis/Command/Redis/PUBLISH.php index 55508f8d9..d38cd1949 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPublish.php +++ b/plugins/cache-redis/Predis/Command/Redis/PUBLISH.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/publish - * - * @author Daniele Alessandri + * @see http://redis.io/commands/publish */ -class PubSubPublish extends Command +class PUBLISH extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPubsub.php b/plugins/cache-redis/Predis/Command/Redis/PUBSUB.php similarity index 80% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPubsub.php rename to plugins/cache-redis/Predis/Command/Redis/PUBSUB.php index 8cf812973..cd6396f5b 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/PubSubPubsub.php +++ b/plugins/cache-redis/Predis/Command/Redis/PUBSUB.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/pubsub - * - * @author Daniele Alessandri + * @see http://redis.io/commands/pubsub */ -class PubSubPubsub extends Command +class PUBSUB extends RedisCommand { /** * {@inheritdoc} @@ -49,7 +50,7 @@ class PubSubPubsub extends Command */ protected static function processNumsub(array $channels) { - $processed = array(); + $processed = []; $count = count($channels); for ($i = 0; $i < $count; ++$i) { diff --git a/plugins/cache-redis/Predis/Command/Redis/PUNSUBSCRIBE.php b/plugins/cache-redis/Predis/Command/Redis/PUNSUBSCRIBE.php new file mode 100644 index 000000000..f15a39fbb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/PUNSUBSCRIBE.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/quit - * - * @author Daniele Alessandri + * @see http://redis.io/commands/quit */ -class ConnectionQuit extends Command +class QUIT extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRandom.php b/plugins/cache-redis/Predis/Command/Redis/RANDOMKEY.php similarity index 66% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRandom.php rename to plugins/cache-redis/Predis/Command/Redis/RANDOMKEY.php index b208b2db4..c77e56243 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRandom.php +++ b/plugins/cache-redis/Predis/Command/Redis/RANDOMKEY.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/randomkey - * - * @author Daniele Alessandri + * @see http://redis.io/commands/randomkey */ -class KeyRandom extends Command +class RANDOMKEY extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRename.php b/plugins/cache-redis/Predis/Command/Redis/RENAME.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRename.php rename to plugins/cache-redis/Predis/Command/Redis/RENAME.php index 82e44fb2e..78a216ec0 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyRename.php +++ b/plugins/cache-redis/Predis/Command/Redis/RENAME.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/rename - * - * @author Daniele Alessandri + * @see http://redis.io/commands/rename */ -class KeyRename extends Command +class RENAME extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/RENAMENX.php b/plugins/cache-redis/Predis/Command/Redis/RENAMENX.php new file mode 100644 index 000000000..ce1306fb7 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/RENAMENX.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/restore - * - * @author Daniele Alessandri + * @see http://redis.io/commands/restore */ -class KeyRestore extends Command +class RESTORE extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLast.php b/plugins/cache-redis/Predis/Command/Redis/RPOP.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLast.php rename to plugins/cache-redis/Predis/Command/Redis/RPOP.php index 9e92db5f4..3bc3f977f 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLast.php +++ b/plugins/cache-redis/Predis/Command/Redis/RPOP.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/rpop - * - * @author Daniele Alessandri + * @see http://redis.io/commands/rpop */ -class ListPopLast extends Command +class RPOP extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLastPushHead.php b/plugins/cache-redis/Predis/Command/Redis/RPOPLPUSH.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLastPushHead.php rename to plugins/cache-redis/Predis/Command/Redis/RPOPLPUSH.php index f430eb227..d158f2d55 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ListPopLastPushHead.php +++ b/plugins/cache-redis/Predis/Command/Redis/RPOPLPUSH.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/rpoplpush - * - * @author Daniele Alessandri + * @see http://redis.io/commands/rpoplpush */ -class ListPopLastPushHead extends Command +class RPOPLPUSH extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/RPUSH.php b/plugins/cache-redis/Predis/Command/Redis/RPUSH.php new file mode 100644 index 000000000..a8a72f018 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/RPUSH.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/rpushx - * - * @author Daniele Alessandri + * @see http://redis.io/commands/rpushx */ -class ListPushTailX extends Command +class RPUSHX extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/SADD.php b/plugins/cache-redis/Predis/Command/Redis/SADD.php new file mode 100644 index 000000000..e2213448b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SADD.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/save - * - * @author Daniele Alessandri + * @see http://redis.io/commands/save */ -class ServerSave extends Command +class SAVE extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyScan.php b/plugins/cache-redis/Predis/Command/Redis/SCAN.php similarity index 77% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/KeyScan.php rename to plugins/cache-redis/Predis/Command/Redis/SCAN.php index 05f5bb3ac..bf1da9625 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/KeyScan.php +++ b/plugins/cache-redis/Predis/Command/Redis/SCAN.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/scan - * - * @author Daniele Alessandri + * @see http://redis.io/commands/scan */ -class KeyScan extends Command +class SCAN extends RedisCommand { /** * {@inheritdoc} @@ -29,14 +30,14 @@ class KeyScan extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 2 && is_array($arguments[1])) { $options = $this->prepareOptions(array_pop($arguments)); $arguments = array_merge($arguments, $options); } - return $arguments; + parent::setArguments($arguments); } /** @@ -49,7 +50,7 @@ class KeyScan extends Command protected function prepareOptions($options) { $options = array_change_key_case($options, CASE_UPPER); - $normalized = array(); + $normalized = []; if (!empty($options['MATCH'])) { $normalized[] = 'MATCH'; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetCardinality.php b/plugins/cache-redis/Predis/Command/Redis/SCARD.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/SetCardinality.php rename to plugins/cache-redis/Predis/Command/Redis/SCARD.php index a9f959b78..daf1393da 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetCardinality.php +++ b/plugins/cache-redis/Predis/Command/Redis/SCARD.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/scard - * - * @author Daniele Alessandri + * @see http://redis.io/commands/scard */ -class SetCardinality extends Command +class SCARD extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerScript.php b/plugins/cache-redis/Predis/Command/Redis/SCRIPT.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerScript.php rename to plugins/cache-redis/Predis/Command/Redis/SCRIPT.php index 7a01018d9..5df20cf72 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerScript.php +++ b/plugins/cache-redis/Predis/Command/Redis/SCRIPT.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/script - * - * @author Daniele Alessandri + * @see http://redis.io/commands/script */ -class ServerScript extends Command +class SCRIPT extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/SDIFF.php b/plugins/cache-redis/Predis/Command/Redis/SDIFF.php new file mode 100644 index 000000000..b59e63df8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SDIFF.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/select - * - * @author Daniele Alessandri + * @see http://redis.io/commands/select */ -class ConnectionSelect extends Command +class SELECT extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSentinel.php b/plugins/cache-redis/Predis/Command/Redis/SENTINEL.php similarity index 73% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSentinel.php rename to plugins/cache-redis/Predis/Command/Redis/SENTINEL.php index c0962db3d..bd22ffba5 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSentinel.php +++ b/plugins/cache-redis/Predis/Command/Redis/SENTINEL.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/topics/sentinel - * - * @author Daniele Alessandri + * @see http://redis.io/topics/sentinel */ -class ServerSentinel extends Command +class SENTINEL extends RedisCommand { /** * {@inheritdoc} @@ -31,7 +32,10 @@ class ServerSentinel extends Command */ public function parseResponse($data) { - switch (strtolower($this->getArgument(0))) { + $argument = $this->getArgument(0); + $argument = is_null($argument) ? null : strtolower($argument); + + switch ($argument) { case 'masters': case 'slaves': return self::processMastersOrSlaves($data); @@ -51,7 +55,7 @@ class ServerSentinel extends Command protected static function processMastersOrSlaves(array $servers) { foreach ($servers as $idx => $node) { - $processed = array(); + $processed = []; $count = count($node); for ($i = 0; $i < $count; ++$i) { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSet.php b/plugins/cache-redis/Predis/Command/Redis/SET.php similarity index 59% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringSet.php rename to plugins/cache-redis/Predis/Command/Redis/SET.php index b1469945c..f8956f40b 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSet.php +++ b/plugins/cache-redis/Predis/Command/Redis/SET.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/set - * - * @author Daniele Alessandri + * @see http://redis.io/commands/set */ -class StringSet extends Command +class SET extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetBit.php b/plugins/cache-redis/Predis/Command/Redis/SETBIT.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetBit.php rename to plugins/cache-redis/Predis/Command/Redis/SETBIT.php index 7933b6be3..6c602ae1d 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetBit.php +++ b/plugins/cache-redis/Predis/Command/Redis/SETBIT.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/setbit - * - * @author Daniele Alessandri + * @see http://redis.io/commands/setbit */ -class StringSetBit extends Command +class SETBIT extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetExpire.php b/plugins/cache-redis/Predis/Command/Redis/SETEX.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetExpire.php rename to plugins/cache-redis/Predis/Command/Redis/SETEX.php index f08817085..66189128f 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringSetExpire.php +++ b/plugins/cache-redis/Predis/Command/Redis/SETEX.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/setex - * - * @author Daniele Alessandri + * @see http://redis.io/commands/setex */ -class StringSetExpire extends Command +class SETEX extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/SETNX.php b/plugins/cache-redis/Predis/Command/Redis/SETNX.php new file mode 100644 index 000000000..d34796504 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SETNX.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/sdiff - * - * @author Daniele Alessandri + * @see http://redis.io/commands/setrange */ -class SetDifference extends SetIntersection +class SETRANGE extends RedisCommand { /** * {@inheritdoc} */ public function getId() { - return 'SDIFF'; + return 'SETRANGE'; } } diff --git a/plugins/cache-redis/Predis/Command/Redis/SHUTDOWN.php b/plugins/cache-redis/Predis/Command/Redis/SHUTDOWN.php new file mode 100644 index 000000000..4d2b74794 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SHUTDOWN.php @@ -0,0 +1,61 @@ +setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetIntersectionStore.php b/plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php similarity index 53% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/SetIntersectionStore.php rename to plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php index b748618aa..144335a1f 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetIntersectionStore.php +++ b/plugins/cache-redis/Predis/Command/Redis/SINTERSTORE.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/sinterstore - * - * @author Daniele Alessandri + * @see http://redis.io/commands/sinterstore */ -class SetIntersectionStore extends Command +class SINTERSTORE extends RedisCommand { /** * {@inheritdoc} @@ -29,12 +30,12 @@ class SetIntersectionStore extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 2 && is_array($arguments[1])) { - return array_merge(array($arguments[0]), $arguments[1]); + $arguments = array_merge([$arguments[0]], $arguments[1]); } - return $arguments; + parent::setArguments($arguments); } } diff --git a/plugins/cache-redis/Predis/Command/Redis/SISMEMBER.php b/plugins/cache-redis/Predis/Command/Redis/SISMEMBER.php new file mode 100644 index 000000000..3991c4c9e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SISMEMBER.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/slaveof - * - * @author Daniele Alessandri + * @see http://redis.io/commands/slaveof */ -class ServerSlaveOf extends Command +class SLAVEOF extends RedisCommand { /** * {@inheritdoc} @@ -29,12 +30,12 @@ class ServerSlaveOf extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 0 || $arguments[0] === 'NO ONE') { - return array('NO', 'ONE'); + $arguments = ['NO', 'ONE']; } - return $arguments; + parent::setArguments($arguments); } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSlowlog.php b/plugins/cache-redis/Predis/Command/Redis/SLOWLOG.php similarity index 71% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSlowlog.php rename to plugins/cache-redis/Predis/Command/Redis/SLOWLOG.php index 137ff59e7..41fbbcdc4 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ServerSlowlog.php +++ b/plugins/cache-redis/Predis/Command/Redis/SLOWLOG.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/slowlog - * - * @author Daniele Alessandri + * @see http://redis.io/commands/slowlog */ -class ServerSlowlog extends Command +class SLOWLOG extends RedisCommand { /** * {@inheritdoc} @@ -32,15 +33,15 @@ class ServerSlowlog extends Command public function parseResponse($data) { if (is_array($data)) { - $log = array(); + $log = []; foreach ($data as $index => $entry) { - $log[$index] = array( + $log[$index] = [ 'id' => $entry[0], 'timestamp' => $entry[1], 'duration' => $entry[2], 'command' => $entry[3], - ); + ]; } return $log; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetMembers.php b/plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/SetMembers.php rename to plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php index f4076ae8b..8f32be40e 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetMembers.php +++ b/plugins/cache-redis/Predis/Command/Redis/SMEMBERS.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/smembers - * - * @author Daniele Alessandri + * @see http://redis.io/commands/smembers */ -class SetMembers extends Command +class SMEMBERS extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/SMISMEMBER.php b/plugins/cache-redis/Predis/Command/Redis/SMISMEMBER.php new file mode 100644 index 000000000..735b01d4f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SMISMEMBER.php @@ -0,0 +1,28 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/sort - * - * @author Daniele Alessandri + * @see http://redis.io/commands/sort */ -class KeySort extends Command +class SORT extends RedisCommand { /** * {@inheritdoc} @@ -29,13 +30,15 @@ class KeySort extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 1) { - return $arguments; + parent::setArguments($arguments); + + return; } - $query = array($arguments[0]); + $query = [$arguments[0]]; $sortParams = array_change_key_case($arguments[1], CASE_UPPER); if (isset($sortParams['BY'])) { @@ -57,9 +60,9 @@ class KeySort extends Command } } - if (isset($sortParams['LIMIT']) && - is_array($sortParams['LIMIT']) && - count($sortParams['LIMIT']) == 2) { + if (isset($sortParams['LIMIT']) + && is_array($sortParams['LIMIT']) + && count($sortParams['LIMIT']) == 2) { $query[] = 'LIMIT'; $query[] = $sortParams['LIMIT'][0]; $query[] = $sortParams['LIMIT'][1]; @@ -78,6 +81,6 @@ class KeySort extends Command $query[] = $sortParams['STORE']; } - return $query; + parent::setArguments($query); } } diff --git a/plugins/cache-redis/Predis/Command/Redis/SORT_RO.php b/plugins/cache-redis/Predis/Command/Redis/SORT_RO.php new file mode 100644 index 000000000..f302b6e3b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SORT_RO.php @@ -0,0 +1,74 @@ +setSorting($arguments); + $arguments = $this->getArguments(); + + $this->setGetArgument($arguments); + $arguments = $this->getArguments(); + + $this->setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setBy($arguments); + $this->filterArguments(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetPop.php b/plugins/cache-redis/Predis/Command/Redis/SPOP.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/SetPop.php rename to plugins/cache-redis/Predis/Command/Redis/SPOP.php index b78d3f33b..e09a3d55a 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetPop.php +++ b/plugins/cache-redis/Predis/Command/Redis/SPOP.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/spop - * - * @author Daniele Alessandri + * @see http://redis.io/commands/spop */ -class SetPop extends Command +class SPOP extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetRandomMember.php b/plugins/cache-redis/Predis/Command/Redis/SRANDMEMBER.php similarity index 57% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/SetRandomMember.php rename to plugins/cache-redis/Predis/Command/Redis/SRANDMEMBER.php index 2cb79a049..61d8f03b5 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/SetRandomMember.php +++ b/plugins/cache-redis/Predis/Command/Redis/SRANDMEMBER.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/srandmember - * - * @author Daniele Alessandri + * @see http://redis.io/commands/srandmember */ -class SetRandomMember extends Command +class SRANDMEMBER extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/SREM.php b/plugins/cache-redis/Predis/Command/Redis/SREM.php new file mode 100644 index 000000000..ad3cf062f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SREM.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/sscan - * - * @author Daniele Alessandri + * @see http://redis.io/commands/sscan */ -class SetScan extends Command +class SSCAN extends RedisCommand { /** * {@inheritdoc} @@ -29,14 +30,14 @@ class SetScan extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 3 && is_array($arguments[2])) { $options = $this->prepareOptions(array_pop($arguments)); $arguments = array_merge($arguments, $options); } - return $arguments; + parent::setArguments($arguments); } /** @@ -49,7 +50,7 @@ class SetScan extends Command protected function prepareOptions($options) { $options = array_change_key_case($options, CASE_UPPER); - $normalized = array(); + $normalized = []; if (!empty($options['MATCH'])) { $normalized[] = 'MATCH'; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringStrlen.php b/plugins/cache-redis/Predis/Command/Redis/STRLEN.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/StringStrlen.php rename to plugins/cache-redis/Predis/Command/Redis/STRLEN.php index 10f492fd9..f51775662 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/StringStrlen.php +++ b/plugins/cache-redis/Predis/Command/Redis/STRLEN.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/strlen - * - * @author Daniele Alessandri + * @see http://redis.io/commands/strlen */ -class StringStrlen extends Command +class STRLEN extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/SUBSCRIBE.php b/plugins/cache-redis/Predis/Command/Redis/SUBSCRIBE.php new file mode 100644 index 000000000..b133c2252 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SUBSCRIBE.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/substr - * - * @author Daniele Alessandri + * @see http://redis.io/commands/substr */ -class StringSubstr extends Command +class SUBSTR extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/SUNION.php b/plugins/cache-redis/Predis/Command/Redis/SUNION.php new file mode 100644 index 000000000..7a94e8da2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/SUNION.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$index, $query], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASADD.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASADD.php new file mode 100644 index 000000000..a2511ea32 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTALIASADD.php @@ -0,0 +1,28 @@ +toArray() : []; + + $schema = array_reduce($schema, static function (array $carry, FieldInterface $field) { + return array_merge($carry, $field->toArray()); + }, []); + + array_unshift($schema, 'SCHEMA', 'ADD'); + + parent::setArguments(array_merge( + [$index], + $commandArguments, + $schema + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTCONFIG.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTCONFIG.php new file mode 100644 index 000000000..9cba60e05 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTCONFIG.php @@ -0,0 +1,30 @@ +toArray() : []; + + $schema = array_reduce($schema, static function (array $carry, FieldInterface $field) { + return array_merge($carry, $field->toArray()); + }, []); + + array_unshift($schema, 'SCHEMA'); + + parent::setArguments(array_merge( + [$index], + $commandArguments, + $schema + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTCURSOR.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTCURSOR.php new file mode 100644 index 000000000..14a01948a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTCURSOR.php @@ -0,0 +1,34 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$subcommand, $index, $cursorId], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTADD.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTADD.php new file mode 100644 index 000000000..c0bc3da72 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTDICTADD.php @@ -0,0 +1,28 @@ +toArray(); + } + + parent::setArguments(array_merge( + [$index], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTEXPLAIN.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTEXPLAIN.php new file mode 100644 index 000000000..e2aa35d47 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTEXPLAIN.php @@ -0,0 +1,43 @@ +toArray(); + } + + parent::setArguments(array_merge( + [$index, $query], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTINFO.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTINFO.php new file mode 100644 index 000000000..02fa95974 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTINFO.php @@ -0,0 +1,28 @@ +toArray() + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSEARCH.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSEARCH.php new file mode 100644 index 000000000..9fbe8a563 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSEARCH.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$index, $query], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSPELLCHECK.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSPELLCHECK.php new file mode 100644 index 000000000..ac0232bb2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSPELLCHECK.php @@ -0,0 +1,38 @@ +toArray(); + } + + parent::setArguments(array_merge( + [$index, $query], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGADD.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGADD.php new file mode 100644 index 000000000..71c42a4a3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGADD.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $string, $score], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGDEL.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGDEL.php new file mode 100644 index 000000000..15457d10b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGDEL.php @@ -0,0 +1,28 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $prefix], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGLEN.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGLEN.php new file mode 100644 index 000000000..1e11d8d30 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTSUGLEN.php @@ -0,0 +1,28 @@ +toArray(); + } + + $terms = array_slice($arguments, 3); + + parent::setArguments(array_merge( + [$index, $synonymGroupId], + $commandArguments, + $terms + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/Search/FTTAGVALS.php b/plugins/cache-redis/Predis/Command/Redis/Search/FTTAGVALS.php new file mode 100644 index 000000000..b323949fe --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/Search/FTTAGVALS.php @@ -0,0 +1,28 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/time - * - * @author Daniele Alessandri + * @see http://redis.io/commands/time */ -class ServerTime extends Command +class TIME extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/TOUCH.php b/plugins/cache-redis/Predis/Command/Redis/TOUCH.php new file mode 100644 index 000000000..82c13fef8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TOUCH.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/ttl - * - * @author Daniele Alessandri + * @see http://redis.io/commands/ttl */ -class KeyTimeToLive extends Command +class TTL extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/TYPE.php b/plugins/cache-redis/Predis/Command/Redis/TYPE.php new file mode 100644 index 000000000..dece63778 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TYPE.php @@ -0,0 +1,51 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $timestamp, $value], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSALTER.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSALTER.php new file mode 100644 index 000000000..8797d68a3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSALTER.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATE.php new file mode 100644 index 000000000..0d88072c0 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATE.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATERULE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATERULE.php new file mode 100644 index 000000000..c8bd62bcb --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSCREATERULE.php @@ -0,0 +1,40 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $value], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDEL.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDEL.php new file mode 100644 index 000000000..747c5a629 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSDEL.php @@ -0,0 +1,28 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINCRBY.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINCRBY.php new file mode 100644 index 000000000..3141e5ded --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINCRBY.php @@ -0,0 +1,41 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $value], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINFO.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINFO.php new file mode 100644 index 000000000..d4941d407 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSINFO.php @@ -0,0 +1,39 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMADD.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMADD.php new file mode 100644 index 000000000..085224693 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMADD.php @@ -0,0 +1,28 @@ +toArray(); + + array_push($processedArguments, 'FILTER', ...$arguments); + + parent::setArguments(array_merge( + $commandArguments, + $processedArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMRANGE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMRANGE.php new file mode 100644 index 000000000..3d68cdd99 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMRANGE.php @@ -0,0 +1,39 @@ +toArray(); + + parent::setArguments(array_merge( + [$fromTimestamp, $toTimestamp], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMREVRANGE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMREVRANGE.php new file mode 100644 index 000000000..987fd8201 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSMREVRANGE.php @@ -0,0 +1,26 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $fromTimestamp, $toTimestamp], + $commandArguments + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSREVRANGE.php b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSREVRANGE.php new file mode 100644 index 000000000..1e8768e96 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TimeSeries/TSREVRANGE.php @@ -0,0 +1,26 @@ +filterArguments(); + } + + public function parseResponse($data) + { + if ($this->isWithCountModifier()) { + $result = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) { + if (array_key_exists($i + 1, $data)) { + $result[(string) $data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } + + /** + * Checks for the presence of the WITHCOUNT modifier. + * + * @return bool + */ + private function isWithCountModifier(): bool + { + $arguments = $this->getArguments(); + $lastArgument = (!empty($arguments)) ? $arguments[count($arguments) - 1] : null; + + return is_string($lastArgument) && strtoupper($lastArgument) === 'WITHCOUNT'; + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKQUERY.php b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKQUERY.php new file mode 100644 index 000000000..128902194 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/TopK/TOPKQUERY.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/unwatch - * - * @author Daniele Alessandri + * @see http://redis.io/commands/unwatch */ -class TransactionUnwatch extends Command +class UNWATCH extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/WAITAOF.php b/plugins/cache-redis/Predis/Command/Redis/WAITAOF.php new file mode 100644 index 000000000..f58cc0547 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/WAITAOF.php @@ -0,0 +1,29 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/watch - * - * @author Daniele Alessandri + * @see http://redis.io/commands/watch */ -class TransactionWatch extends Command +class WATCH extends RedisCommand { /** * {@inheritdoc} @@ -29,12 +30,12 @@ class TransactionWatch extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (isset($arguments[0]) && is_array($arguments[0])) { - return $arguments[0]; + $arguments = $arguments[0]; } - return $arguments; + parent::setArguments($arguments); } } diff --git a/plugins/cache-redis/Predis/Command/Redis/XADD.php b/plugins/cache-redis/Predis/Command/Redis/XADD.php new file mode 100644 index 000000000..21c132272 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/XADD.php @@ -0,0 +1,64 @@ + $val) { + $args[] = $key; + $args[] = $val; + } + } + + parent::setArguments($args); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/XDEL.php b/plugins/cache-redis/Predis/Command/Redis/XDEL.php new file mode 100644 index 000000000..f1c509e35 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/XDEL.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zadd - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zadd */ -class ZSetAdd extends Command +class ZADD extends RedisCommand { /** * {@inheritdoc} @@ -29,7 +30,7 @@ class ZSetAdd extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (is_array(end($arguments))) { foreach (array_pop($arguments) as $member => $score) { @@ -38,6 +39,6 @@ class ZSetAdd extends Command } } - return $arguments; + parent::setArguments($arguments); } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCardinality.php b/plugins/cache-redis/Predis/Command/Redis/ZCARD.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCardinality.php rename to plugins/cache-redis/Predis/Command/Redis/ZCARD.php index 10332009a..7f6cfd4d0 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCardinality.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZCARD.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zcard - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zcard */ -class ZSetCardinality extends Command +class ZCARD extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCount.php b/plugins/cache-redis/Predis/Command/Redis/ZCOUNT.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCount.php rename to plugins/cache-redis/Predis/Command/Redis/ZCOUNT.php index 918bd2b80..32b54cb33 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetCount.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZCOUNT.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zcount - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zcount */ -class ZSetCount extends Command +class ZCOUNT extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZDIFF.php b/plugins/cache-redis/Predis/Command/Redis/ZDIFF.php new file mode 100644 index 000000000..573dcae68 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZDIFF.php @@ -0,0 +1,48 @@ +setKeys($arguments); + $arguments = $this->getArguments(); + + $this->setWithScore($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZDIFFSTORE.php b/plugins/cache-redis/Predis/Command/Redis/ZDIFFSTORE.php new file mode 100644 index 000000000..729988372 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZDIFFSTORE.php @@ -0,0 +1,40 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zincrby - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zincrby */ -class ZSetIncrementBy extends Command +class ZINCRBY extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZINTER.php b/plugins/cache-redis/Predis/Command/Redis/ZINTER.php new file mode 100644 index 000000000..787db2bbe --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZINTER.php @@ -0,0 +1,35 @@ +setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetIntersectionStore.php b/plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php similarity index 57% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetIntersectionStore.php rename to plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php index 572a7a324..7f0aa4498 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetIntersectionStore.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZINTERSTORE.php @@ -3,20 +3,19 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; /** - * @link http://redis.io/commands/zinterstore - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zinterstore */ -class ZSetIntersectionStore extends ZSetUnionStore +class ZINTERSTORE extends ZUNIONSTORE { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetLexCount.php b/plugins/cache-redis/Predis/Command/Redis/ZLEXCOUNT.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetLexCount.php rename to plugins/cache-redis/Predis/Command/Redis/ZLEXCOUNT.php index 447b8eb32..c216b99d9 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetLexCount.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZLEXCOUNT.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zlexcount - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zlexcount */ -class ZSetLexCount extends Command +class ZLEXCOUNT extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZMPOP.php b/plugins/cache-redis/Predis/Command/Redis/ZMPOP.php new file mode 100644 index 000000000..4adc0cca9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZMPOP.php @@ -0,0 +1,79 @@ +setCount($arguments); + $arguments = $this->getArguments(); + + $this->resolveModifier(static::$modifierArgumentPositionOffset, $arguments); + + $this->setKeys($arguments); + $arguments = $this->getArguments(); + + parent::setArguments($arguments); + } + + public function parseResponse($data) + { + $key = array_shift($data); + + if (null === $key) { + return [$key]; + } + + $data = $data[0]; + $parsedData = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; $i++) { + for ($j = 0, $jMax = count($data[$i]); $j < $jMax; ++$j) { + if ($data[$i][$j + 1] ?? false) { + $parsedData[$data[$i][$j]] = $data[$i][++$j]; + } + } + } + + return array_combine([$key], [$parsedData]); + } +} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZMSCORE.php b/plugins/cache-redis/Predis/Command/Redis/ZMSCORE.php new file mode 100644 index 000000000..2dd76fd65 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZMSCORE.php @@ -0,0 +1,34 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zrange - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zrange */ -class ZSetRange extends Command +class ZRANGE extends RedisCommand { /** * {@inheritdoc} @@ -29,25 +30,24 @@ class ZSetRange extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 4) { $lastType = gettype($arguments[3]); if ($lastType === 'string' && strtoupper($arguments[3]) === 'WITHSCORES') { // Used for compatibility with older versions - $arguments[3] = array('WITHSCORES' => true); + $arguments[3] = ['WITHSCORES' => true]; $lastType = 'array'; } if ($lastType === 'array') { $options = $this->prepareOptions(array_pop($arguments)); - - return array_merge($arguments, $options); + $arguments = array_merge($arguments, $options); } } - return $arguments; + parent::setArguments($arguments); } /** @@ -60,7 +60,7 @@ class ZSetRange extends Command protected function prepareOptions($options) { $opts = array_change_key_case($options, CASE_UPPER); - $finalizedOpts = array(); + $finalizedOpts = []; if (!empty($opts['WITHSCORES'])) { $finalizedOpts[] = 'WITHSCORES'; @@ -91,10 +91,14 @@ class ZSetRange extends Command public function parseResponse($data) { if ($this->withScores()) { - $result = array(); + $result = []; for ($i = 0; $i < count($data); ++$i) { - $result[$data[$i]] = $data[++$i]; + if (is_array($data[$i])) { + $result[$data[$i][0]] = $data[$i][1]; // Relay + } else { + $result[$data[$i]] = $data[++$i]; + } } return $result; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByLex.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php similarity index 65% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByLex.php rename to plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php index 9b2991a81..18b4a6d30 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByLex.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYLEX.php @@ -3,20 +3,19 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; /** - * @link http://redis.io/commands/zrangebylex - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zrangebylex */ -class ZSetRangeByLex extends ZSetRange +class ZRANGEBYLEX extends ZRANGE { /** * {@inheritdoc} @@ -32,14 +31,14 @@ class ZSetRangeByLex extends ZSetRange protected function prepareOptions($options) { $opts = array_change_key_case($options, CASE_UPPER); - $finalizedOpts = array(); + $finalizedOpts = []; if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) { $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER); $finalizedOpts[] = 'LIMIT'; - $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0]; - $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1]; + $finalizedOpts[] = $limit['OFFSET'] ?? $limit[0]; + $finalizedOpts[] = $limit['COUNT'] ?? $limit[1]; } return $finalizedOpts; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByScore.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYSCORE.php similarity index 73% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByScore.php rename to plugins/cache-redis/Predis/Command/Redis/ZRANGEBYSCORE.php index 961a5bc2e..66cbe4eab 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRangeByScore.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGEBYSCORE.php @@ -3,20 +3,19 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; /** - * @link http://redis.io/commands/zrangebyscore - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zrangebyscore */ -class ZSetRangeByScore extends ZSetRange +class ZRANGEBYSCORE extends ZRANGE { /** * {@inheritdoc} @@ -32,14 +31,14 @@ class ZSetRangeByScore extends ZSetRange protected function prepareOptions($options) { $opts = array_change_key_case($options, CASE_UPPER); - $finalizedOpts = array(); + $finalizedOpts = []; if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) { $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER); $finalizedOpts[] = 'LIMIT'; - $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0]; - $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1]; + $finalizedOpts[] = $limit['OFFSET'] ?? $limit[0]; + $finalizedOpts[] = $limit['COUNT'] ?? $limit[1]; } return array_merge($finalizedOpts, parent::prepareOptions($options)); diff --git a/plugins/cache-redis/Predis/Command/Redis/ZRANGESTORE.php b/plugins/cache-redis/Predis/Command/Redis/ZRANGESTORE.php new file mode 100644 index 000000000..4f820b06c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANGESTORE.php @@ -0,0 +1,57 @@ +setByLexByScoreArgument($arguments); + $arguments = $this->getArguments(); + + $this->setReversedArgument($arguments); + $arguments = $this->getArguments(); + + $this->setLimitArguments($arguments); + $this->filterArguments(); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRank.php b/plugins/cache-redis/Predis/Command/Redis/ZRANK.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRank.php rename to plugins/cache-redis/Predis/Command/Redis/ZRANK.php index d0c9c536e..3c6ac2a6e 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRank.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZRANK.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zrank - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zrank */ -class ZSetRank extends Command +class ZRANK extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZREM.php b/plugins/cache-redis/Predis/Command/Redis/ZREM.php new file mode 100644 index 000000000..f14a0790a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZREM.php @@ -0,0 +1,39 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zremrangebylex - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zremrangebylex */ -class ZSetRemoveRangeByLex extends Command +class ZREMRANGEBYLEX extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByRank.php b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYRANK.php similarity index 57% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByRank.php rename to plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYRANK.php index 89cd5baff..3aeff30c0 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByRank.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYRANK.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zremrangebyrank - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zremrangebyrank */ -class ZSetRemoveRangeByRank extends Command +class ZREMRANGEBYRANK extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByScore.php b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYSCORE.php similarity index 57% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByScore.php rename to plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYSCORE.php index a7c30814b..6f88c5d1d 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetRemoveRangeByScore.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZREMRANGEBYSCORE.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zremrangebyscore - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zremrangebyscore */ -class ZSetRemoveRangeByScore extends Command +class ZREMRANGEBYSCORE extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRange.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGE.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRange.php rename to plugins/cache-redis/Predis/Command/Redis/ZREVRANGE.php index 6a46a7a5a..a22566191 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRange.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGE.php @@ -3,20 +3,19 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; /** - * @link http://redis.io/commands/zrevrange - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zrevrange */ -class ZSetReverseRange extends ZSetRange +class ZREVRANGE extends ZRANGE { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByLex.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYLEX.php similarity index 61% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByLex.php rename to plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYLEX.php index cdd8ba623..75cad0bce 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByLex.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYLEX.php @@ -3,15 +3,19 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; -class ZSetReverseRangeByLex extends ZSetRangeByLex +/** + * @see http://redis.io/commands/zrevrangebylex + */ +class ZREVRANGEBYLEX extends ZRANGEBYLEX { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByScore.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYSCORE.php similarity index 57% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByScore.php rename to plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYSCORE.php index 1078eb72b..9acc450df 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRangeByScore.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANGEBYSCORE.php @@ -3,20 +3,19 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; /** - * @link http://redis.io/commands/zrevrangebyscore - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zrevrangebyscore */ -class ZSetReverseRangeByScore extends ZSetRangeByScore +class ZREVRANGEBYSCORE extends ZRANGEBYSCORE { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRank.php b/plugins/cache-redis/Predis/Command/Redis/ZREVRANK.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRank.php rename to plugins/cache-redis/Predis/Command/Redis/ZREVRANK.php index 33fb81584..620d2fe70 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetReverseRank.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZREVRANK.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zrevrank - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zrevrank */ -class ZSetReverseRank extends Command +class ZREVRANK extends RedisCommand { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScan.php b/plugins/cache-redis/Predis/Command/Redis/ZSCAN.php similarity index 80% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScan.php rename to plugins/cache-redis/Predis/Command/Redis/ZSCAN.php index 1dc2352ed..ee0b13105 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScan.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZSCAN.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zscan - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zscan */ -class ZSetScan extends Command +class ZSCAN extends RedisCommand { /** * {@inheritdoc} @@ -29,14 +30,14 @@ class ZSetScan extends Command /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (count($arguments) === 3 && is_array($arguments[2])) { $options = $this->prepareOptions(array_pop($arguments)); $arguments = array_merge($arguments, $options); } - return $arguments; + parent::setArguments($arguments); } /** @@ -49,7 +50,7 @@ class ZSetScan extends Command protected function prepareOptions($options) { $options = array_change_key_case($options, CASE_UPPER); - $normalized = array(); + $normalized = []; if (!empty($options['MATCH'])) { $normalized[] = 'MATCH'; @@ -71,7 +72,7 @@ class ZSetScan extends Command { if (is_array($data)) { $members = $data[1]; - $result = array(); + $result = []; for ($i = 0; $i < count($members); ++$i) { $result[$members[$i]] = (float) $members[++$i]; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScore.php b/plugins/cache-redis/Predis/Command/Redis/ZSCORE.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScore.php rename to plugins/cache-redis/Predis/Command/Redis/ZSCORE.php index 2e7fce8ed..978a17292 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ZSetScore.php +++ b/plugins/cache-redis/Predis/Command/Redis/ZSCORE.php @@ -3,20 +3,21 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Command; +namespace Predis\Command\Redis; + +use Predis\Command\Command as RedisCommand; /** - * @link http://redis.io/commands/zscore - * - * @author Daniele Alessandri + * @see http://redis.io/commands/zscore */ -class ZSetScore extends Command +class ZSCORE extends RedisCommand { /** * {@inheritdoc} diff --git a/plugins/cache-redis/Predis/Command/Redis/ZUNION.php b/plugins/cache-redis/Predis/Command/Redis/ZUNION.php new file mode 100644 index 000000000..e0e7f233f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Redis/ZUNION.php @@ -0,0 +1,35 @@ +setAggregate($arguments); + $arguments = $this->getArguments(); + + $this->setWeights($arguments); + $arguments = $this->getArguments(); + + $this->setKeys($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/RedisFactory.php b/plugins/cache-redis/Predis/Command/RedisFactory.php new file mode 100644 index 000000000..10c4541d5 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/RedisFactory.php @@ -0,0 +1,112 @@ +commands = [ + 'ECHO' => 'Predis\Command\Redis\ECHO_', + 'EVAL' => 'Predis\Command\Redis\EVAL_', + 'OBJECT' => 'Predis\Command\Redis\OBJECT_', + // Class name corresponds to PHP reserved word "function", added mapping to bypass restrictions + 'FUNCTION' => FUNCTIONS::class, + ]; + } + + /** + * {@inheritdoc} + */ + public function getCommandClass(string $commandID): ?string + { + $commandID = strtoupper($commandID); + + if (isset($this->commands[$commandID]) || array_key_exists($commandID, $this->commands)) { + return $this->commands[$commandID]; + } + + $commandClass = $this->resolve($commandID); + + if (null === $commandClass) { + return null; + } + + $this->commands[$commandID] = $commandClass; + + return $commandClass; + } + + /** + * {@inheritdoc} + */ + public function undefine(string $commandID): void + { + // NOTE: we explicitly associate `NULL` to the command ID in the map + // instead of the parent's `unset()` because our subclass tries to load + // a predefined class from the Predis\Command\Redis namespace when no + // explicit mapping is defined, see RedisFactory::getCommandClass() for + // details of the implementation of this mechanism. + $this->commands[strtoupper($commandID)] = null; + } + + /** + * Resolves command object from given command ID. + * + * @param string $commandID Command ID of virtual method call + * @return string|null FQDN of corresponding command object + */ + private function resolve(string $commandID): ?string + { + if (class_exists($commandClass = self::COMMANDS_NAMESPACE . '\\' . $commandID)) { + return $commandClass; + } + + $commandModule = $this->resolveCommandModuleByPrefix($commandID); + + if (null === $commandModule) { + return null; + } + + if (class_exists($commandClass = self::COMMANDS_NAMESPACE . '\\' . $commandModule . '\\' . $commandID)) { + return $commandClass; + } + + return null; + } + + private function resolveCommandModuleByPrefix(string $commandID): ?string + { + foreach (ClientConfiguration::getModules() as $module) { + if (preg_match("/^{$module['commandPrefix']}/", $commandID)) { + return $module['name']; + } + } + + return null; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Command/ScriptCommand.php b/plugins/cache-redis/Predis/Command/ScriptCommand.php similarity index 62% rename from snappymail/v/0.0.0/app/libraries/Predis/Command/ScriptCommand.php rename to plugins/cache-redis/Predis/Command/ScriptCommand.php index a30bc1d28..330ee94b3 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Command/ScriptCommand.php +++ b/plugins/cache-redis/Predis/Command/ScriptCommand.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,12 +16,18 @@ namespace Predis\Command; * Base class used to implement an higher level abstraction for commands based * on Lua scripting with EVAL and EVALSHA. * - * @link http://redis.io/commands/eval - * - * @author Daniele Alessandri + * @see http://redis.io/commands/eval */ -abstract class ScriptCommand extends ServerEvalSHA +abstract class ScriptCommand extends Command { + /** + * {@inheritdoc} + */ + public function getId() + { + return 'EVALSHA'; + } + /** * Gets the body of a Lua script. * @@ -28,6 +35,16 @@ abstract class ScriptCommand extends ServerEvalSHA */ abstract public function getScript(); + /** + * Calculates the SHA1 hash of the body of the script. + * + * @return string SHA1 hash. + */ + public function getScriptHash() + { + return sha1($this->getScript()); + } + /** * Specifies the number of arguments that should be considered as keys. * @@ -55,16 +72,20 @@ abstract class ScriptCommand extends ServerEvalSHA /** * {@inheritdoc} */ - protected function filterArguments(array $arguments) + public function setArguments(array $arguments) { if (($numkeys = $this->getKeysCount()) && $numkeys < 0) { $numkeys = count($arguments) + $numkeys; } - return array_merge(array(sha1($this->getScript()), (int) $numkeys), $arguments); + $arguments = array_merge([$this->getScriptHash(), (int) $numkeys], $arguments); + + parent::setArguments($arguments); } /** + * Returns arguments for EVAL command. + * * @return array */ public function getEvalArguments() @@ -74,4 +95,14 @@ abstract class ScriptCommand extends ServerEvalSHA return $arguments; } + + /** + * Returns the equivalent EVAL command as a raw command instance. + * + * @return RawCommand + */ + public function getEvalCommand() + { + return new RawCommand('EVAL', $this->getEvalArguments()); + } } diff --git a/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DeleteStrategy.php b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DeleteStrategy.php new file mode 100644 index 000000000..250ae744e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Strategy/ContainerCommands/Functions/DeleteStrategy.php @@ -0,0 +1,26 @@ +separator = $separator; + } + + /** + * {@inheritDoc} + */ + public function resolve(string $commandId, string $subcommandId): SubcommandStrategyInterface + { + $subcommandStrategyClass = ucwords($subcommandId) . 'Strategy'; + $commandDirectoryName = ucwords($commandId); + + if (!is_null($this->separator)) { + $subcommandStrategyClass = str_replace($this->separator, '', $subcommandStrategyClass); + $commandDirectoryName = str_replace($this->separator, '', $commandDirectoryName); + } + + if (class_exists( + $containerCommandClass = self::CONTAINER_COMMANDS_NAMESPACE . '\\' . $commandDirectoryName . '\\' . $subcommandStrategyClass + )) { + return new $containerCommandClass(); + } + + throw new InvalidArgumentException('Non-existing container command given'); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Aggregate.php b/plugins/cache-redis/Predis/Command/Traits/Aggregate.php new file mode 100644 index 000000000..c49c31085 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Aggregate.php @@ -0,0 +1,66 @@ + 'MIN', + 'max' => 'MAX', + 'sum' => 'SUM', + ]; + + /** + * @var string + */ + private static $aggregateModifier = 'AGGREGATE'; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$aggregateArgumentPositionOffset >= $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$aggregateArgumentPositionOffset]; + + if (is_string($argument) && in_array(strtoupper($argument), self::$aggregateValuesEnum)) { + $argument = self::$aggregateValuesEnum[$argument]; + } else { + $enumValues = implode(', ', array_keys(self::$aggregateValuesEnum)); + throw new UnexpectedValueException("Aggregate argument accepts only: {$enumValues} values"); + } + + $argumentsBefore = array_slice($arguments, 0, static::$aggregateArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$aggregateArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$aggregateModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BitByte.php b/plugins/cache-redis/Predis/Command/Traits/BitByte.php new file mode 100644 index 000000000..067302d22 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BitByte.php @@ -0,0 +1,40 @@ + 'BIT', + 'byte' => 'BYTE', + ]; + + public function setArguments(array $arguments) + { + $value = array_pop($arguments); + + if (null === $value) { + parent::setArguments($arguments); + + return; + } + + if (in_array(strtoupper($value), self::$argumentEnum, true)) { + $arguments[] = self::$argumentEnum[$value]; + } else { + $arguments[] = $value; + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/BucketSize.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/BucketSize.php new file mode 100644 index 000000000..99e22be05 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/BucketSize.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$bucketSizeArgumentPositionOffset] === -1) { + array_splice($arguments, static::$bucketSizeArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$bucketSizeArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong bucket size argument value or position offset'); + } + + $argument = $arguments[static::$bucketSizeArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$bucketSizeArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$bucketSizeArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$bucketSizeModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Capacity.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Capacity.php new file mode 100644 index 000000000..c0dccc8a4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Capacity.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$capacityArgumentPositionOffset] === -1) { + array_splice($arguments, static::$capacityArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$capacityArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong capacity argument value or position offset'); + } + + $argument = $arguments[static::$capacityArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$capacityArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$capacityArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$capacityModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Error.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Error.php new file mode 100644 index 000000000..661def80f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Error.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$errorArgumentPositionOffset] === -1) { + array_splice($arguments, static::$errorArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$errorArgumentPositionOffset] < 0) { + throw new UnexpectedValueException('Wrong error argument value or position offset'); + } + + $argument = $arguments[static::$errorArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$errorArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$errorArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$errorModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Expansion.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Expansion.php new file mode 100644 index 000000000..74d916f4c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Expansion.php @@ -0,0 +1,53 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$expansionArgumentPositionOffset] === -1) { + array_splice($arguments, static::$expansionArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$expansionArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong expansion argument value or position offset'); + } + + $argument = $arguments[static::$expansionArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$expansionArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$expansionArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$expansionModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Items.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Items.php new file mode 100644 index 000000000..9d2e4dcfe --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/Items.php @@ -0,0 +1,45 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$itemsArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$itemsArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$itemsArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$itemsModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/MaxIterations.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/MaxIterations.php new file mode 100644 index 000000000..fb307e6d9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/MaxIterations.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$maxIterationsArgumentPositionOffset] === -1) { + array_splice($arguments, static::$maxIterationsArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$maxIterationsArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong max iterations argument value or position offset'); + } + + $argument = $arguments[static::$maxIterationsArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$maxIterationsArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$maxIterationsArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$maxIterationsModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/BloomFilters/NoCreate.php b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/NoCreate.php new file mode 100644 index 000000000..7fc084ec8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/BloomFilters/NoCreate.php @@ -0,0 +1,49 @@ += $argumentsLength + || false === $arguments[static::$noCreateArgumentPositionOffset] + ) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$noCreateArgumentPositionOffset]; + + if (true === $argument) { + $argument = 'NOCREATE'; + } else { + throw new UnexpectedValueException('Wrong NOCREATE argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$noCreateArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$noCreateArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/By/ByArgument.php b/plugins/cache-redis/Predis/Command/Traits/By/ByArgument.php new file mode 100644 index 000000000..99bd1722f --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/By/ByArgument.php @@ -0,0 +1,40 @@ += $argumentsLength || null === $arguments[static::$byArgumentPositionOffset]) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$byArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$byArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$byArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$this->byModifier, $argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/By/ByLexByScore.php b/plugins/cache-redis/Predis/Command/Traits/By/ByLexByScore.php new file mode 100644 index 000000000..66c531584 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/By/ByLexByScore.php @@ -0,0 +1,49 @@ + 'BYLEX', + 'byscore' => 'BYSCORE', + ]; + + public function setArguments(array $arguments) + { + $argument = $arguments[static::$byLexByScoreArgumentPositionOffset]; + + if (false === $argument) { + parent::setArguments($arguments); + + return; + } + + if (is_string($argument) && in_array(strtoupper($argument), self::$argumentsEnum)) { + $argument = self::$argumentsEnum[$argument]; + } else { + throw new UnexpectedValueException('By argument accepts only "bylex" and "byscore" values'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$byLexByScoreArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$byLexByScoreArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/By/GeoBy.php b/plugins/cache-redis/Predis/Command/Traits/By/GeoBy.php new file mode 100644 index 000000000..c5ee2b7d1 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/By/GeoBy.php @@ -0,0 +1,49 @@ +getByArgumentPositionOffset($arguments); + + if (null === $argumentPositionOffset) { + throw new InvalidArgumentException('Invalid BY argument value given'); + } + + $byArgumentObject = $arguments[$argumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset); + $argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + $byArgumentObject->toArray(), + $argumentsAfter + )); + } + + private function getByArgumentPositionOffset(array $arguments): ?int + { + foreach ($arguments as $i => $value) { + if ($value instanceof ByInterface) { + return $i; + } + } + + return null; + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Count.php b/plugins/cache-redis/Predis/Command/Traits/Count.php new file mode 100644 index 000000000..46ae489d6 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Count.php @@ -0,0 +1,71 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$countArgumentPositionOffset] === -1) { + array_splice($arguments, static::$countArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$countArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong count argument value or position offset'); + } + + $countArgument = $arguments[static::$countArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$countArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$countArgumentPositionOffset + 2); + + if (!$any) { + $argumentsAfter = array_slice($arguments, static::$countArgumentPositionOffset + 1); + parent::setArguments(array_merge( + $argumentsBefore, + [$this->countModifier], + [$countArgument], + $argumentsAfter + )); + + return; + } + + parent::setArguments(array_merge( + $argumentsBefore, + [$this->countModifier], + [$countArgument], + [$this->anyModifier], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/DB.php b/plugins/cache-redis/Predis/Command/Traits/DB.php new file mode 100644 index 000000000..cf494f96a --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/DB.php @@ -0,0 +1,53 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if (!is_numeric($arguments[static::$dbArgumentPositionOffset])) { + throw new UnexpectedValueException('DB argument should be a valid numeric value'); + } + + if ($arguments[static::$dbArgumentPositionOffset] < 0) { + array_splice($arguments, static::$dbArgumentPositionOffset, 1); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$dbArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$dbArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$dbArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [$this->dbModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Expire/ExpireOptions.php b/plugins/cache-redis/Predis/Command/Traits/Expire/ExpireOptions.php new file mode 100644 index 000000000..4f683f3ca --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Expire/ExpireOptions.php @@ -0,0 +1,42 @@ + 'NX', + 'xx' => 'XX', + 'gt' => 'GT', + 'lt' => 'LT', + ]; + + public function setArguments(array $arguments) + { + $value = array_pop($arguments); + + if (null === $value) { + parent::setArguments($arguments); + + return; + } + + if (in_array(strtoupper($value), self::$argumentEnum, true)) { + $arguments[] = self::$argumentEnum[strtolower($value)]; + } else { + $arguments[] = $value; + } + + parent::setArguments($arguments); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/From/GeoFrom.php b/plugins/cache-redis/Predis/Command/Traits/From/GeoFrom.php new file mode 100644 index 000000000..22688adba --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/From/GeoFrom.php @@ -0,0 +1,49 @@ +getFromArgumentPositionOffset($arguments); + + if (null === $argumentPositionOffset) { + throw new InvalidArgumentException('Invalid FROM argument value given'); + } + + $fromArgumentObject = $arguments[$argumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset); + $argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + $fromArgumentObject->toArray(), + $argumentsAfter + )); + } + + private function getFromArgumentPositionOffset(array $arguments): ?int + { + foreach ($arguments as $i => $value) { + if ($value instanceof FromInterface) { + return $i; + } + } + + return null; + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Get/Get.php b/plugins/cache-redis/Predis/Command/Traits/Get/Get.php new file mode 100644 index 000000000..256676e9e --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Get/Get.php @@ -0,0 +1,47 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if (!is_array($arguments[static::$getArgumentPositionOffset])) { + throw new UnexpectedValueException('Wrong get argument type'); + } + + $patterns = []; + + foreach ($arguments[static::$getArgumentPositionOffset] as $pattern) { + $patterns[] = self::$getModifier; + $patterns[] = $pattern; + } + + $argumentsBeforeKeys = array_slice($arguments, 0, static::$getArgumentPositionOffset); + $argumentsAfterKeys = array_slice($arguments, static::$getArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBeforeKeys, $patterns, $argumentsAfterKeys)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/Indent.php b/plugins/cache-redis/Predis/Command/Traits/Json/Indent.php new file mode 100644 index 000000000..3e0dfb133 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/Indent.php @@ -0,0 +1,54 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$indentArgumentPositionOffset] === '') { + array_splice($arguments, static::$indentArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$indentArgumentPositionOffset]; + + if (!is_string($argument)) { + throw new UnexpectedValueException('Indent argument value should be a string'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$indentArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$indentArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$indentModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/Newline.php b/plugins/cache-redis/Predis/Command/Traits/Json/Newline.php new file mode 100644 index 000000000..7bab8205b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/Newline.php @@ -0,0 +1,54 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$newlineArgumentPositionOffset] === '') { + array_splice($arguments, static::$newlineArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$newlineArgumentPositionOffset]; + + if (!is_string($argument)) { + throw new UnexpectedValueException('Newline argument value should be a string'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$newlineArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$newlineArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$newlineModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/NxXxArgument.php b/plugins/cache-redis/Predis/Command/Traits/Json/NxXxArgument.php new file mode 100644 index 000000000..39d3cb234 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/NxXxArgument.php @@ -0,0 +1,64 @@ + 'NX', + 'xx' => 'XX', + ]; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$nxXxArgumentPositionOffset >= $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if (null === $arguments[static::$nxXxArgumentPositionOffset]) { + array_splice($arguments, static::$nxXxArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$nxXxArgumentPositionOffset]; + + if (!in_array(strtoupper($argument), self::$argumentEnum, true)) { + $enumValues = implode(', ', array_keys(self::$argumentEnum)); + throw new UnexpectedValueException("Argument accepts only: {$enumValues} values"); + } + + $argumentsBefore = array_slice($arguments, 0, static::$nxXxArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$nxXxArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$argumentEnum[strtolower($argument)]], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Json/Space.php b/plugins/cache-redis/Predis/Command/Traits/Json/Space.php new file mode 100644 index 000000000..5c99828f4 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Json/Space.php @@ -0,0 +1,54 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$spaceArgumentPositionOffset] === '') { + array_splice($arguments, static::$spaceArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$spaceArgumentPositionOffset]; + + if (!is_string($argument)) { + throw new UnexpectedValueException('Space argument value should be a string'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$spaceArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$spaceArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$spaceModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Keys.php b/plugins/cache-redis/Predis/Command/Traits/Keys.php new file mode 100644 index 000000000..5dc86ad7d --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Keys.php @@ -0,0 +1,47 @@ + $argumentsLength + || !is_array($arguments[static::$keysArgumentPositionOffset]) + ) { + throw new UnexpectedValueException('Wrong keys argument type or position offset'); + } + + $keysArgument = $arguments[static::$keysArgumentPositionOffset]; + $argumentsBeforeKeys = array_slice($arguments, 0, static::$keysArgumentPositionOffset); + $argumentsAfterKeys = array_slice($arguments, static::$keysArgumentPositionOffset + 1); + + if ($withNumkeys) { + $numkeys = count($keysArgument); + parent::setArguments(array_merge($argumentsBeforeKeys, [$numkeys], $keysArgument, $argumentsAfterKeys)); + + return; + } + + parent::setArguments(array_merge($argumentsBeforeKeys, $keysArgument, $argumentsAfterKeys)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/LeftRight.php b/plugins/cache-redis/Predis/Command/Traits/LeftRight.php new file mode 100644 index 000000000..181fbd143 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/LeftRight.php @@ -0,0 +1,60 @@ + 'LEFT', + 'right' => 'RIGHT', + ]; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$leftRightArgumentPositionOffset >= $argumentsLength) { + $arguments[] = 'LEFT'; + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$leftRightArgumentPositionOffset]; + + if (is_string($argument) && in_array(strtoupper($argument), self::$leftRightEnum, true)) { + $argument = self::$leftRightEnum[$argument]; + } else { + $enumValues = implode(', ', array_keys(self::$leftRightEnum)); + throw new UnexpectedValueException("Left/Right argument accepts only: {$enumValues} values"); + } + + $argumentsBefore = array_slice($arguments, 0, static::$leftRightArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$leftRightArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Limit/Limit.php b/plugins/cache-redis/Predis/Command/Traits/Limit/Limit.php new file mode 100644 index 000000000..e24499472 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Limit/Limit.php @@ -0,0 +1,54 @@ += $argumentsLength + || false === $arguments[static::$limitArgumentPositionOffset] + ) { + parent::setArguments($argumentsBefore); + + return; + } + + $argument = $arguments[static::$limitArgumentPositionOffset]; + $argumentsAfter = array_slice($arguments, static::$limitArgumentPositionOffset + 1); + + if (true === $argument) { + parent::setArguments(array_merge($argumentsBefore, [self::$limitModifier], $argumentsAfter)); + + return; + } + + if (!is_int($argument)) { + throw new UnexpectedValueException('Wrong limit argument type'); + } + + parent::setArguments(array_merge($argumentsBefore, [self::$limitModifier], [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Limit/LimitObject.php b/plugins/cache-redis/Predis/Command/Traits/Limit/LimitObject.php new file mode 100644 index 000000000..3e47de9ab --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Limit/LimitObject.php @@ -0,0 +1,50 @@ +getLimitArgumentPositionOffset($arguments); + + if (null === $argumentPositionOffset) { + parent::setArguments($arguments); + + return; + } + + $limitObject = $arguments[$argumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset); + $argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + $limitObject->toArray(), + $argumentsAfter + )); + } + + private function getLimitArgumentPositionOffset(array $arguments): ?int + { + foreach ($arguments as $i => $value) { + if ($value instanceof LimitInterface) { + return $i; + } + } + + return null; + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/MinMaxModifier.php b/plugins/cache-redis/Predis/Command/Traits/MinMaxModifier.php new file mode 100644 index 000000000..f48f4c1e8 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/MinMaxModifier.php @@ -0,0 +1,45 @@ + 'MIN', + 'max' => 'MAX', + ]; + + public function resolveModifier(int $offset, array &$arguments): void + { + if ($offset >= count($arguments)) { + $arguments[$offset] = $this->modifierEnum['min']; + + return; + } + + if (!is_string($arguments[$offset]) || !array_key_exists($arguments[$offset], $this->modifierEnum)) { + throw new UnexpectedValueException('Wrong type of modifier given'); + } + + $arguments[$offset] = $this->modifierEnum[$arguments[$offset]]; + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Replace.php b/plugins/cache-redis/Predis/Command/Traits/Replace.php new file mode 100644 index 000000000..d193d66d3 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Replace.php @@ -0,0 +1,34 @@ + 'ASC', + 'desc' => 'DESC', + ]; + + public function setArguments(array $arguments) + { + $argumentsLength = count($arguments); + + if (static::$sortArgumentPositionOffset >= $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$sortArgumentPositionOffset]; + + if (null === $argument) { + array_splice($arguments, static::$sortArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if (!in_array(strtoupper($argument), self::$sortingEnum, true)) { + $enumValues = implode(', ', array_keys(self::$sortingEnum)); + throw new UnexpectedValueException("Sorting argument accepts only: {$enumValues} values"); + } + + $argumentsBefore = array_slice($arguments, 0, static::$sortArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$sortArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$sortingEnum[$argument]], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Storedist.php b/plugins/cache-redis/Predis/Command/Traits/Storedist.php new file mode 100644 index 000000000..6feed1e8c --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Storedist.php @@ -0,0 +1,49 @@ += $argumentsLength + || false === $arguments[static::$storeDistArgumentPositionOffset] + ) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$storeDistArgumentPositionOffset]; + + if (true === $argument) { + $argument = 'STOREDIST'; + } else { + throw new UnexpectedValueException('Wrong STOREDIST argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$storeDistArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$storeDistArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Timeout.php b/plugins/cache-redis/Predis/Command/Traits/Timeout.php new file mode 100644 index 000000000..fd33ea9cf --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Timeout.php @@ -0,0 +1,53 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$timeoutArgumentPositionOffset] === -1) { + array_splice($arguments, static::$timeoutArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$timeoutArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong timeout argument value or position offset'); + } + + $argument = $arguments[static::$timeoutArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$timeoutArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$timeoutArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$timeoutModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/To/ServerTo.php b/plugins/cache-redis/Predis/Command/Traits/To/ServerTo.php new file mode 100644 index 000000000..1ab13eca9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/To/ServerTo.php @@ -0,0 +1,48 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + /** @var To|null $toArgument */ + $toArgument = $arguments[static::$toArgumentPositionOffset]; + + if (null === $toArgument) { + array_splice($arguments, static::$toArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + $argumentsBefore = array_slice($arguments, 0, static::$toArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$toArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + $toArgument->toArray(), + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/Weights.php b/plugins/cache-redis/Predis/Command/Traits/Weights.php new file mode 100644 index 000000000..1f175ed8b --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/Weights.php @@ -0,0 +1,61 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if (!is_array($arguments[static::$weightsArgumentPositionOffset])) { + throw new UnexpectedValueException('Wrong weights argument type'); + } + + $weightsArray = $arguments[static::$weightsArgumentPositionOffset]; + + if (empty($weightsArray)) { + unset($arguments[static::$weightsArgumentPositionOffset]); + parent::setArguments($arguments); + + return; + } + + $argumentsBefore = array_slice($arguments, 0, static::$weightsArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$weightsArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$weightsModifier], + $weightsArray, + $argumentsAfter + )); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithCoord.php b/plugins/cache-redis/Predis/Command/Traits/With/WithCoord.php new file mode 100644 index 000000000..797ae2e52 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithCoord.php @@ -0,0 +1,49 @@ += $argumentsLength + || false === $arguments[static::$withCoordArgumentPositionOffset] + ) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$withCoordArgumentPositionOffset]; + + if (true === $argument) { + $argument = 'WITHCOORD'; + } else { + throw new UnexpectedValueException('Wrong WITHCOORD argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$withCoordArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$withCoordArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithDist.php b/plugins/cache-redis/Predis/Command/Traits/With/WithDist.php new file mode 100644 index 000000000..479606dca --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithDist.php @@ -0,0 +1,45 @@ += $argumentsLength + || false === $arguments[static::$withDistArgumentPositionOffset] + ) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$withDistArgumentPositionOffset]; + + if (true === $argument) { + $argument = 'WITHDIST'; + } else { + throw new UnexpectedValueException('Wrong WITHDIST argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$withDistArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$withDistArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithHash.php b/plugins/cache-redis/Predis/Command/Traits/With/WithHash.php new file mode 100644 index 000000000..c00f680b2 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithHash.php @@ -0,0 +1,45 @@ += $argumentsLength + || false === $arguments[static::$withHashArgumentPositionOffset] + ) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$withHashArgumentPositionOffset]; + + if (true === $argument) { + $argument = 'WITHHASH'; + } else { + throw new UnexpectedValueException('Wrong WITHHASH argument type'); + } + + $argumentsBefore = array_slice($arguments, 0, static::$withHashArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$withHashArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithScores.php b/plugins/cache-redis/Predis/Command/Traits/With/WithScores.php new file mode 100644 index 000000000..bc81d36c9 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithScores.php @@ -0,0 +1,68 @@ +isWithScoreModifier()) { + $result = []; + + for ($i = 0, $iMax = count($data); $i < $iMax; ++$i) { + if (is_array($data[$i])) { + $result[$data[$i][0]] = $data[$i][1]; // Relay + } elseif (array_key_exists($i + 1, $data)) { + $result[$data[$i]] = $data[++$i]; + } + } + + return $result; + } + + return $data; + } +} diff --git a/plugins/cache-redis/Predis/Command/Traits/With/WithValues.php b/plugins/cache-redis/Predis/Command/Traits/With/WithValues.php new file mode 100644 index 000000000..4efb06584 --- /dev/null +++ b/plugins/cache-redis/Predis/Command/Traits/With/WithValues.php @@ -0,0 +1,34 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,12 +12,11 @@ namespace Predis; +use Exception; use Predis\Connection\NodeConnectionInterface; /** * Base exception class for network-related errors. - * - * @author Daniele Alessandri */ abstract class CommunicationException extends PredisException { @@ -26,15 +26,20 @@ abstract class CommunicationException extends PredisException * @param NodeConnectionInterface $connection Connection that generated the exception. * @param string $message Error message. * @param int $code Error code. - * @param \Exception $innerException Inner exception for wrapping the original error. + * @param Exception|null $innerException Inner exception for wrapping the original error. */ public function __construct( NodeConnectionInterface $connection, - $message = null, - $code = null, - \Exception $innerException = null + $message = '', + $code = 0, + Exception $innerException = null ) { - parent::__construct($message, $code, $innerException); + parent::__construct( + is_null($message) ? '' : $message, + is_null($code) ? 0 : $code, + $innerException + ); + $this->connection = $connection; } diff --git a/plugins/cache-redis/Predis/Configuration/Option/Aggregate.php b/plugins/cache-redis/Predis/Configuration/Option/Aggregate.php new file mode 100644 index 000000000..262f8d64d --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Aggregate.php @@ -0,0 +1,114 @@ +getConnectionInitializer($options, $value); + } + + /** + * Wraps a user-supplied callable used to create a new aggregate connection. + * + * When the original callable acting as a connection initializer is executed + * by the client to create a new aggregate connection, it will receive the + * following arguments: + * + * - $parameters (same as passed to Predis\Client::__construct()) + * - $options (options container, Predis\Configuration\OptionsInterface) + * - $option (current option, Predis\Configuration\OptionInterface) + * + * The original callable must return a valid aggregation connection instance + * of type Predis\Connection\AggregateConnectionInterface, this is enforced + * by the wrapper returned by this method and an exception is thrown when + * invalid values are returned. + * + * @param OptionsInterface $options Client options + * @param callable $callable Callable initializer + * + * @return callable + * @throws InvalidArgumentException + */ + protected function getConnectionInitializer(OptionsInterface $options, callable $callable) + { + return function ($parameters = null, $autoaggregate = false) use ($callable, $options) { + $connection = call_user_func_array($callable, [&$parameters, $options, $this]); + + if (!$connection instanceof AggregateConnectionInterface) { + throw new InvalidArgumentException(sprintf( + '%s expects the supplied callable to return an instance of %s, but %s was returned', + static::class, + AggregateConnectionInterface::class, + is_object($connection) ? get_class($connection) : gettype($connection) + )); + } + + if ($parameters && $autoaggregate) { + static::aggregate($options, $connection, $parameters); + } + + return $connection; + }; + } + + /** + * Adds single connections to an aggregate connection instance. + * + * @param OptionsInterface $options Client options + * @param AggregateConnectionInterface $connection Target aggregate connection + * @param array $nodes List of nodes to be added to the target aggregate connection + */ + public static function aggregate(OptionsInterface $options, AggregateConnectionInterface $connection, array $nodes) + { + $connections = $options->connections; + + foreach ($nodes as $node) { + $connection->add($node instanceof NodeConnectionInterface ? $node : $connections->create($node)); + } + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return; + } +} diff --git a/plugins/cache-redis/Predis/Configuration/Option/CRC16.php b/plugins/cache-redis/Predis/Configuration/Option/CRC16.php new file mode 100644 index 000000000..b17144922 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/CRC16.php @@ -0,0 +1,74 @@ +getHashGeneratorByDescription($options, $value); + } elseif ($value instanceof Hash\HashGeneratorInterface) { + return $value; + } else { + $class = get_class($this); + throw new InvalidArgumentException("$class expects a valid hash generator"); + } + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return function_exists('phpiredis_utils_crc16') + ? new Hash\PhpiredisCRC16() + : new Hash\CRC16(); + } +} diff --git a/plugins/cache-redis/Predis/Configuration/Option/Cluster.php b/plugins/cache-redis/Predis/Configuration/Option/Cluster.php new file mode 100644 index 000000000..34b33de47 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Cluster.php @@ -0,0 +1,99 @@ +getConnectionInitializerByString($options, $value); + } + + if (is_callable($value)) { + return $this->getConnectionInitializer($options, $value); + } else { + throw new InvalidArgumentException(sprintf( + '%s expects either a string or a callable value, %s given', + static::class, + is_object($value) ? get_class($value) : gettype($value) + )); + } + } + + /** + * Returns a connection initializer from a descriptive name. + * + * @param OptionsInterface $options Client options + * @param string $description Identifier of a replication backend (`predis`, `sentinel`) + * + * @return callable + */ + protected function getConnectionInitializerByString(OptionsInterface $options, string $description) + { + switch ($description) { + case 'redis': + case 'redis-cluster': + return function ($parameters, $options, $option) { + return new RedisCluster($options->connections, new RedisStrategy($options->crc16)); + }; + + case 'predis': + return $this->getDefaultConnectionInitializer(); + + default: + throw new InvalidArgumentException(sprintf( + '%s expects either `predis`, `redis` or `redis-cluster` as valid string values, `%s` given', + static::class, + $description + )); + } + } + + /** + * Returns the default connection initializer. + * + * @return callable + */ + protected function getDefaultConnectionInitializer() + { + return function ($parameters, $options, $option) { + return new PredisCluster(); + }; + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return $this->getConnectionInitializer( + $options, + $this->getDefaultConnectionInitializer() + ); + } +} diff --git a/plugins/cache-redis/Predis/Configuration/Option/Commands.php b/plugins/cache-redis/Predis/Configuration/Option/Commands.php new file mode 100644 index 000000000..2fbe00e74 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Commands.php @@ -0,0 +1,146 @@ +createFactoryByArray($options, $value); + } elseif (is_string($value)) { + return $this->createFactoryByString($options, $value); + } else { + throw new InvalidArgumentException(sprintf( + '%s expects a valid command factory', + static::class + )); + } + } + + /** + * Creates a new default command factory from a named array. + * + * The factory instance is configured according to the supplied named array + * mapping command IDs (passed as keys) to the FCQN of classes implementing + * Predis\Command\CommandInterface. + * + * @param OptionsInterface $options Client options container + * @param array $value Named array mapping command IDs to classes + * + * @return FactoryInterface + */ + protected function createFactoryByArray(OptionsInterface $options, array $value) + { + /** + * @var FactoryInterface + */ + $commands = $this->getDefault($options); + + foreach ($value as $commandID => $commandClass) { + if ($commandClass === null) { + $commands->undefine($commandID); + } else { + $commands->define($commandID, $commandClass); + } + } + + return $commands; + } + + /** + * Creates a new command factory from a descriptive string. + * + * The factory instance is configured according to the supplied descriptive + * string that identifies specific configurations of schemes and connection + * classes. Supported configuration values are: + * + * - "predis" returns the default command factory used by Predis + * - "raw" returns a command factory that creates only raw commands + * - "default" is simply an alias of "predis" + * + * @param OptionsInterface $options Client options container + * @param string $value Descriptive string identifying the desired configuration + * + * @return FactoryInterface + */ + protected function createFactoryByString(OptionsInterface $options, string $value) + { + switch (strtolower($value)) { + case 'default': + case 'predis': + return $this->getDefault($options); + + case 'raw': + return $this->createRawFactory($options); + + default: + throw new InvalidArgumentException(sprintf( + '%s does not recognize `%s` as a supported configuration string', + static::class, + $value + )); + } + } + + /** + * Creates a new raw command factory instance. + * + * @param OptionsInterface $options Client options container + */ + protected function createRawFactory(OptionsInterface $options): FactoryInterface + { + $commands = new RawFactory(); + + if (isset($options->prefix)) { + throw new InvalidArgumentException(sprintf( + '%s does not support key prefixing', RawFactory::class + )); + } + + return $commands; + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + $commands = new RedisFactory(); + + if (isset($options->prefix)) { + $commands->setProcessor($options->prefix); + } + + return $commands; + } +} diff --git a/plugins/cache-redis/Predis/Configuration/Option/Connections.php b/plugins/cache-redis/Predis/Configuration/Option/Connections.php new file mode 100644 index 000000000..e37de4cad --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Connections.php @@ -0,0 +1,152 @@ +createFactoryByArray($options, $value); + } elseif (is_string($value)) { + return $this->createFactoryByString($options, $value); + } else { + throw new InvalidArgumentException(sprintf( + '%s expects a valid connection factory', static::class + )); + } + } + + /** + * Creates a new connection factory from a named array. + * + * The factory instance is configured according to the supplied named array + * mapping URI schemes (passed as keys) to the FCQN of classes implementing + * Predis\Connection\NodeConnectionInterface, or callable objects acting as + * lazy initializers and returning new instances of classes implementing + * Predis\Connection\NodeConnectionInterface. + * + * @param OptionsInterface $options Client options + * @param array $value Named array mapping URI schemes to classes or callables + * + * @return FactoryInterface + */ + protected function createFactoryByArray(OptionsInterface $options, array $value) + { + /** + * @var FactoryInterface + */ + $factory = $this->getDefault($options); + + foreach ($value as $scheme => $initializer) { + $factory->define($scheme, $initializer); + } + + return $factory; + } + + /** + * Creates a new connection factory from a descriptive string. + * + * The factory instance is configured according to the supplied descriptive + * string that identifies specific configurations of schemes and connection + * classes. Supported configuration values are: + * + * - "phpiredis-stream" maps tcp, redis, unix to PhpiredisStreamConnection + * - "phpiredis-socket" maps tcp, redis, unix to PhpiredisSocketConnection + * - "phpiredis" is an alias of "phpiredis-stream" + * - "relay" maps tcp, redis, unix, tls, rediss to RelayConnection + * + * @param OptionsInterface $options Client options + * @param string $value Descriptive string identifying the desired configuration + * + * @return FactoryInterface + */ + protected function createFactoryByString(OptionsInterface $options, string $value) + { + /** + * @var FactoryInterface + */ + $factory = $this->getDefault($options); + + switch (strtolower($value)) { + case 'phpiredis': + case 'phpiredis-stream': + $factory->define('tcp', PhpiredisStreamConnection::class); + $factory->define('redis', PhpiredisStreamConnection::class); + $factory->define('unix', PhpiredisStreamConnection::class); + break; + + case 'phpiredis-socket': + $factory->define('tcp', PhpiredisSocketConnection::class); + $factory->define('redis', PhpiredisSocketConnection::class); + $factory->define('unix', PhpiredisSocketConnection::class); + break; + + case 'relay': + $factory->define('tcp', RelayConnection::class); + $factory->define('redis', RelayConnection::class); + $factory->define('unix', RelayConnection::class); + break; + + case 'default': + return $factory; + + default: + throw new InvalidArgumentException(sprintf( + '%s does not recognize `%s` as a supported configuration string', static::class, $value + )); + } + + return $factory; + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + $factory = new Factory(); + + if ($options->defined('parameters')) { + $factory->setDefaultParameters($options->parameters); + } + + return $factory; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/ExceptionsOption.php b/plugins/cache-redis/Predis/Configuration/Option/Exceptions.php similarity index 73% rename from snappymail/v/0.0.0/app/libraries/Predis/Configuration/ExceptionsOption.php rename to plugins/cache-redis/Predis/Configuration/Option/Exceptions.php index 337733e4b..6834272f0 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/ExceptionsOption.php +++ b/plugins/cache-redis/Predis/Configuration/Option/Exceptions.php @@ -3,21 +3,23 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Configuration; +namespace Predis\Configuration\Option; + +use Predis\Configuration\OptionInterface; +use Predis\Configuration\OptionsInterface; /** * Configures whether consumers (such as the client) should throw exceptions on * Redis errors (-ERR responses) or just return instances of error responses. - * - * @author Daniele Alessandri */ -class ExceptionsOption implements OptionInterface +class Exceptions implements OptionInterface { /** * {@inheritdoc} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/PrefixOption.php b/plugins/cache-redis/Predis/Configuration/Option/Prefix.php similarity index 66% rename from snappymail/v/0.0.0/app/libraries/Predis/Configuration/PrefixOption.php rename to plugins/cache-redis/Predis/Configuration/Option/Prefix.php index 5827cdc37..772454b36 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/PrefixOption.php +++ b/plugins/cache-redis/Predis/Configuration/Option/Prefix.php @@ -3,35 +3,40 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Configuration; +namespace Predis\Configuration\Option; use Predis\Command\Processor\KeyPrefixProcessor; use Predis\Command\Processor\ProcessorInterface; +use Predis\Configuration\OptionInterface; +use Predis\Configuration\OptionsInterface; /** * Configures a command processor that apply the specified prefix string to a * series of Redis commands considered prefixable. - * - * @author Daniele Alessandri */ -class PrefixOption implements OptionInterface +class Prefix implements OptionInterface { /** * {@inheritdoc} */ public function filter(OptionsInterface $options, $value) { + if (is_callable($value)) { + $value = call_user_func($value, $options); + } + if ($value instanceof ProcessorInterface) { return $value; } - return new KeyPrefixProcessor($value); + return new KeyPrefixProcessor((string) $value); } /** diff --git a/plugins/cache-redis/Predis/Configuration/Option/Replication.php b/plugins/cache-redis/Predis/Configuration/Option/Replication.php new file mode 100644 index 000000000..b0f132d19 --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/Option/Replication.php @@ -0,0 +1,126 @@ +getConnectionInitializerByString($options, $value); + } + + if (is_callable($value)) { + return $this->getConnectionInitializer($options, $value); + } else { + throw new InvalidArgumentException(sprintf( + '%s expects either a string or a callable value, %s given', + static::class, + is_object($value) ? get_class($value) : gettype($value) + )); + } + } + + /** + * Returns a connection initializer (callable) from a descriptive string. + * + * Each connection initializer is specialized for the specified replication + * backend so that all the necessary steps for the configuration of the new + * aggregate connection are performed inside the initializer and the client + * receives a ready-to-use connection. + * + * Supported configuration values are: + * + * - `predis` for unmanaged replication setups + * - `redis-sentinel` for replication setups managed by redis-sentinel + * - `sentinel` is an alias of `redis-sentinel` + * + * @param OptionsInterface $options Client options + * @param string $description Identifier of a replication backend + * + * @return callable + */ + protected function getConnectionInitializerByString(OptionsInterface $options, string $description) + { + switch ($description) { + case 'sentinel': + case 'redis-sentinel': + return function ($parameters, $options) { + return new SentinelReplication($options->service, $parameters, $options->connections); + }; + + case 'predis': + return $this->getDefaultConnectionInitializer(); + + default: + throw new InvalidArgumentException(sprintf( + '%s expects either `predis`, `sentinel` or `redis-sentinel` as valid string values, `%s` given', + static::class, + $description + )); + } + } + + /** + * Returns the default connection initializer. + * + * @return callable + */ + protected function getDefaultConnectionInitializer() + { + return function ($parameters, $options) { + $connection = new MasterSlaveReplication(); + + if ($options->autodiscovery) { + $connection->setConnectionFactory($options->connections); + $connection->setAutoDiscovery(true); + } + + return $connection; + }; + } + + /** + * {@inheritdoc} + */ + public static function aggregate(OptionsInterface $options, AggregateConnectionInterface $connection, array $nodes) + { + if (!$connection instanceof SentinelReplication) { + parent::aggregate($options, $connection, $nodes); + } + } + + /** + * {@inheritdoc} + */ + public function getDefault(OptionsInterface $options) + { + return $this->getConnectionInitializer( + $options, + $this->getDefaultConnectionInitializer() + ); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/OptionInterface.php b/plugins/cache-redis/Predis/Configuration/OptionInterface.php similarity index 89% rename from snappymail/v/0.0.0/app/libraries/Predis/Configuration/OptionInterface.php rename to plugins/cache-redis/Predis/Configuration/OptionInterface.php index b31e0c98f..538fc0ba7 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/OptionInterface.php +++ b/plugins/cache-redis/Predis/Configuration/OptionInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,8 +15,6 @@ namespace Predis\Configuration; /** * Defines an handler used by Predis\Configuration\Options to filter, validate * or return default values for a given option. - * - * @author Daniele Alessandri */ interface OptionInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/Options.php b/plugins/cache-redis/Predis/Configuration/Options.php similarity index 53% rename from snappymail/v/0.0.0/app/libraries/Predis/Configuration/Options.php rename to plugins/cache-redis/Predis/Configuration/Options.php index 6f3b331b5..3fff04129 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Configuration/Options.php +++ b/plugins/cache-redis/Predis/Configuration/Options.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,44 +13,39 @@ namespace Predis\Configuration; /** - * Manages Predis options with filtering, conversion and lazy initialization of - * values using a mini-DI container approach. + * Default client options container for Predis\Client. + * + * Pre-defined options have their specialized handlers that can filter, convert + * an lazily initialize values in a mini-DI container approach. * * {@inheritdoc} - * - * @author Daniele Alessandri */ class Options implements OptionsInterface { + /** @var array */ + protected $handlers = [ + 'aggregate' => Option\Aggregate::class, + 'cluster' => Option\Cluster::class, + 'replication' => Option\Replication::class, + 'connections' => Option\Connections::class, + 'commands' => Option\Commands::class, + 'exceptions' => Option\Exceptions::class, + 'prefix' => Option\Prefix::class, + 'crc16' => Option\CRC16::class, + ]; + + /** @var array */ + protected $options = []; + + /** @var array */ protected $input; - protected $options; - protected $handlers; /** - * @param array $options Array of options with their values + * @param array $options Named array of client options */ - public function __construct(array $options = array()) + public function __construct(array $options = null) { - $this->input = $options; - $this->options = array(); - $this->handlers = $this->getHandlers(); - } - - /** - * Ensures that the default options are initialized. - * - * @return array - */ - protected function getHandlers() - { - return array( - 'cluster' => 'Predis\Configuration\ClusterOption', - 'connections' => 'Predis\Configuration\ConnectionFactoryOption', - 'exceptions' => 'Predis\Configuration\ExceptionsOption', - 'prefix' => 'Predis\Configuration\PrefixOption', - 'profile' => 'Predis\Configuration\ProfileOption', - 'replication' => 'Predis\Configuration\ReplicationOption', - ); + $this->input = $options ?? []; } /** @@ -70,10 +66,10 @@ class Options implements OptionsInterface */ public function defined($option) { - return ( - array_key_exists($option, $this->options) || - array_key_exists($option, $this->input) - ); + return + array_key_exists($option, $this->options) + || array_key_exists($option, $this->input) + ; } /** @@ -82,8 +78,8 @@ class Options implements OptionsInterface public function __isset($option) { return ( - array_key_exists($option, $this->options) || - array_key_exists($option, $this->input) + array_key_exists($option, $this->options) + || array_key_exists($option, $this->input) ) && $this->__get($option) !== null; } @@ -100,14 +96,12 @@ class Options implements OptionsInterface $value = $this->input[$option]; unset($this->input[$option]); - if (is_object($value) && method_exists($value, '__invoke')) { - $value = $value($this, $option); - } - if (isset($this->handlers[$option])) { $handler = $this->handlers[$option]; $handler = new $handler(); $value = $handler->filter($this, $value); + } elseif (is_object($value) && method_exists($value, '__invoke')) { + $value = $value($this); } return $this->options[$option] = $value; diff --git a/plugins/cache-redis/Predis/Configuration/OptionsInterface.php b/plugins/cache-redis/Predis/Configuration/OptionsInterface.php new file mode 100644 index 000000000..597a0579b --- /dev/null +++ b/plugins/cache-redis/Predis/Configuration/OptionsInterface.php @@ -0,0 +1,63 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,15 +12,15 @@ namespace Predis\Connection; +use InvalidArgumentException; use Predis\Command\CommandInterface; +use Predis\Command\RawCommand; use Predis\CommunicationException; use Predis\Protocol\ProtocolException; /** * Base class with the common logic used by connection classes to communicate * with Redis. - * - * @author Daniele Alessandri */ abstract class AbstractConnection implements NodeConnectionInterface { @@ -27,7 +28,11 @@ abstract class AbstractConnection implements NodeConnectionInterface private $cachedId; protected $parameters; - protected $initCommands = array(); + + /** + * @var RawCommand[] + */ + protected $initCommands = []; /** * @param ParametersInterface $parameters Initialization parameters for the connection. @@ -51,24 +56,10 @@ abstract class AbstractConnection implements NodeConnectionInterface * * @param ParametersInterface $parameters Initialization parameters for the connection. * - * @throws \InvalidArgumentException - * * @return ParametersInterface + * @throws InvalidArgumentException */ - protected function assertParameters(ParametersInterface $parameters) - { - switch ($parameters->scheme) { - case 'tcp': - case 'redis': - case 'unix': - break; - - default: - throw new \InvalidArgumentException("Invalid scheme: '$parameters->scheme'."); - } - - return $parameters; - } + abstract protected function assertParameters(ParametersInterface $parameters); /** * Creates the underlying resource used to communicate with Redis. @@ -115,6 +106,14 @@ abstract class AbstractConnection implements NodeConnectionInterface $this->initCommands[] = $command; } + /** + * {@inheritdoc} + */ + public function getInitCommands(): array + { + return $this->initCommands; + } + /** * {@inheritdoc} */ @@ -133,39 +132,16 @@ abstract class AbstractConnection implements NodeConnectionInterface return $this->read(); } - /** - * Helper method that returns an exception message augmented with useful - * details from the connection parameters. - * - * @param string $message Error message. - * - * @return string - */ - private function createExceptionMessage($message) - { - $parameters = $this->parameters; - - if ($parameters->scheme === 'unix') { - return "$message [$parameters->scheme:$parameters->path]"; - } - - if (filter_var($parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - return "$message [$parameters->scheme://[$parameters->host]:$parameters->port]"; - } - - return "$message [$parameters->scheme://$parameters->host:$parameters->port]"; - } - /** * Helper method to handle connection errors. * * @param string $message Error message. * @param int $code Error code. */ - protected function onConnectionError($message, $code = null) + protected function onConnectionError($message, $code = 0) { CommunicationException::handle( - new ConnectionException($this, static::createExceptionMessage($message), $code) + new ConnectionException($this, "$message [{$this->getParameters()}]", $code) ); } @@ -177,7 +153,7 @@ abstract class AbstractConnection implements NodeConnectionInterface protected function onProtocolError($message) { CommunicationException::handle( - new ProtocolException($this, static::createExceptionMessage($message)) + new ProtocolException($this, "$message [{$this->getParameters()}]") ); } @@ -234,6 +210,6 @@ abstract class AbstractConnection implements NodeConnectionInterface */ public function __sleep() { - return array('parameters', 'initCommands'); + return ['parameters', 'initCommands']; } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/AggregateConnectionInterface.php b/plugins/cache-redis/Predis/Connection/AggregateConnectionInterface.php similarity index 89% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/AggregateConnectionInterface.php rename to plugins/cache-redis/Predis/Connection/AggregateConnectionInterface.php index 7eeaede76..8864bba53 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/AggregateConnectionInterface.php +++ b/plugins/cache-redis/Predis/Connection/AggregateConnectionInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,8 +17,6 @@ use Predis\Command\CommandInterface; /** * Defines a virtual connection composed of multiple connection instances to * single Redis nodes. - * - * @author Daniele Alessandri */ interface AggregateConnectionInterface extends ConnectionInterface { @@ -44,7 +43,7 @@ interface AggregateConnectionInterface extends ConnectionInterface * * @return NodeConnectionInterface */ - public function getConnection(CommandInterface $command); + public function getConnectionByCommand(CommandInterface $command); /** * Returns a connection instance from the aggregate connection by its alias. diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/ClusterInterface.php b/plugins/cache-redis/Predis/Connection/Cluster/ClusterInterface.php similarity index 75% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/ClusterInterface.php rename to plugins/cache-redis/Predis/Connection/Cluster/ClusterInterface.php index af0f5aab5..79c6f96e9 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/ClusterInterface.php +++ b/plugins/cache-redis/Predis/Connection/Cluster/ClusterInterface.php @@ -3,21 +3,20 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Connection\Aggregate; +namespace Predis\Connection\Cluster; use Predis\Connection\AggregateConnectionInterface; /** * Defines a cluster of Redis servers formed by aggregating multiple connection * instances to single Redis nodes. - * - * @author Daniele Alessandri */ interface ClusterInterface extends AggregateConnectionInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/PredisCluster.php b/plugins/cache-redis/Predis/Connection/Cluster/PredisCluster.php similarity index 62% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/PredisCluster.php rename to plugins/cache-redis/Predis/Connection/Cluster/PredisCluster.php index 33f98bf2e..c30fd09d3 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/PredisCluster.php +++ b/plugins/cache-redis/Predis/Connection/Cluster/PredisCluster.php @@ -3,32 +3,50 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Connection\Aggregate; +namespace Predis\Connection\Cluster; +use ArrayIterator; +use Countable; +use IteratorAggregate; use Predis\Cluster\PredisStrategy; use Predis\Cluster\StrategyInterface; use Predis\Command\CommandInterface; use Predis\Connection\NodeConnectionInterface; use Predis\NotSupportedException; +use ReturnTypeWillChange; +use Traversable; /** * Abstraction for a cluster of aggregate connections to various Redis servers * implementing client-side sharding based on pluggable distribution strategies. - * - * @author Daniele Alessandri - * - * @todo Add the ability to remove connections from pool. */ -class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable +class PredisCluster implements ClusterInterface, IteratorAggregate, Countable { - private $pool; + /** + * @var NodeConnectionInterface[] + */ + private $pool = []; + + /** + * @var NodeConnectionInterface[] + */ + private $aliases = []; + + /** + * @var StrategyInterface + */ private $strategy; + + /** + * @var \Predis\Cluster\Distributor\DistributorInterface + */ private $distributor; /** @@ -36,7 +54,6 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ public function __construct(StrategyInterface $strategy = null) { - $this->pool = array(); $this->strategy = $strategy ?: new PredisStrategy(); $this->distributor = $this->strategy->getDistributor(); } @@ -82,14 +99,13 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable { $parameters = $connection->getParameters(); + $this->pool[(string) $connection] = $connection; + if (isset($parameters->alias)) { - $this->pool[$parameters->alias] = $connection; - } else { - $this->pool[] = $connection; + $this->aliases[$parameters->alias] = $connection; } - $weight = isset($parameters->weight) ? $parameters->weight : null; - $this->distributor->add($connection, $weight); + $this->distributor->add($connection, $parameters->weight); } /** @@ -97,36 +113,24 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ public function remove(NodeConnectionInterface $connection) { - if (($id = array_search($connection, $this->pool, true)) !== false) { + if (false !== $id = array_search($connection, $this->pool, true)) { unset($this->pool[$id]); $this->distributor->remove($connection); + if ($this->aliases && $alias = $connection->getParameters()->alias) { + unset($this->aliases[$alias]); + } + return true; } return false; } - /** - * Removes a connection instance using its alias or index. - * - * @param string $connectionID Alias or index of a connection. - * - * @return bool Returns true if the connection was in the pool. - */ - public function removeById($connectionID) - { - if ($connection = $this->getConnectionById($connectionID)) { - return $this->remove($connection); - } - - return false; - } - /** * {@inheritdoc} */ - public function getConnection(CommandInterface $command) + public function getConnectionByCommand(CommandInterface $command) { $slot = $this->strategy->getSlot($command); @@ -136,17 +140,39 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable ); } - $node = $this->distributor->getBySlot($slot); - - return $node; + return $this->distributor->getBySlot($slot); } /** * {@inheritdoc} */ - public function getConnectionById($connectionID) + public function getConnectionById($id) { - return isset($this->pool[$connectionID]) ? $this->pool[$connectionID] : null; + return $this->pool[$id] ?? null; + } + + /** + * Returns a connection instance by its alias. + * + * @param string $alias Connection alias. + * + * @return NodeConnectionInterface|null + */ + public function getConnectionByAlias($alias) + { + return $this->aliases[$alias] ?? null; + } + + /** + * Retrieves a connection instance by slot. + * + * @param string $slot Slot name. + * + * @return NodeConnectionInterface|null + */ + public function getConnectionBySlot($slot) + { + return $this->distributor->getBySlot($slot); } /** @@ -159,9 +185,8 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable public function getConnectionByKey($key) { $hash = $this->strategy->getSlotByKey($key); - $node = $this->distributor->getBySlot($hash); - return $node; + return $this->distributor->getBySlot($hash); } /** @@ -176,19 +201,21 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable } /** - * {@inheritdoc} + * @return int */ + #[ReturnTypeWillChange] public function count() { return count($this->pool); } /** - * {@inheritdoc} + * @return Traversable */ + #[ReturnTypeWillChange] public function getIterator() { - return new \ArrayIterator($this->pool); + return new ArrayIterator($this->pool); } /** @@ -196,7 +223,7 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ public function writeRequest(CommandInterface $command) { - $this->getConnection($command)->writeRequest($command); + $this->getConnectionByCommand($command)->writeRequest($command); } /** @@ -204,7 +231,7 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ public function readResponse(CommandInterface $command) { - return $this->getConnection($command)->readResponse($command); + return $this->getConnectionByCommand($command)->readResponse($command); } /** @@ -212,24 +239,6 @@ class PredisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ public function executeCommand(CommandInterface $command) { - return $this->getConnection($command)->executeCommand($command); - } - - /** - * Executes the specified Redis command on all the nodes of a cluster. - * - * @param CommandInterface $command A Redis command. - * - * @return array - */ - public function executeCommandOnNodes(CommandInterface $command) - { - $responses = array(); - - foreach ($this->pool as $connection) { - $responses[] = $connection->executeCommand($command); - } - - return $responses; + return $this->getConnectionByCommand($command)->executeCommand($command); } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/RedisCluster.php b/plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php similarity index 59% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/RedisCluster.php rename to plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php index 337c28702..7f3013c17 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/RedisCluster.php +++ b/plugins/cache-redis/Predis/Connection/Cluster/RedisCluster.php @@ -3,22 +3,35 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Connection\Aggregate; +namespace Predis\Connection\Cluster; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use OutOfBoundsException; +use Predis\ClientException; use Predis\Cluster\RedisStrategy as RedisClusterStrategy; +use Predis\Cluster\SlotMap; use Predis\Cluster\StrategyInterface; use Predis\Command\CommandInterface; use Predis\Command\RawCommand; +use Predis\Connection\ConnectionException; use Predis\Connection\FactoryInterface; use Predis\Connection\NodeConnectionInterface; use Predis\NotSupportedException; +use Predis\Response\Error as ErrorResponse; use Predis\Response\ErrorInterface as ErrorResponseInterface; +use Predis\Response\ServerException; +use ReturnTypeWillChange; +use Throwable; +use Traversable; /** * Abstraction for a Redis-backed cluster of nodes (Redis >= 3.0.0). @@ -39,18 +52,17 @@ use Predis\Response\ErrorInterface as ErrorResponseInterface; * of the nodes and optionally enable such a behaviour upon -MOVED redirections. * Asking for the cluster configuration to Redis is actually done by issuing a * CLUSTER SLOTS command to a random node in the pool. - * - * @author Daniele Alessandri */ -class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable +class RedisCluster implements ClusterInterface, IteratorAggregate, Countable { private $useClusterSlots = true; - private $defaultParameters = array(); - private $pool = array(); - private $slots = array(); - private $slotsMap; + private $pool = []; + private $slots = []; + private $slotmap; private $strategy; private $connections; + private $retryLimit = 5; + private $retryInterval = 10; /** * @param FactoryInterface $connections Optional connection factory. @@ -62,6 +74,41 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable ) { $this->connections = $connections; $this->strategy = $strategy ?: new RedisClusterStrategy(); + $this->slotmap = new SlotMap(); + } + + /** + * Sets the maximum number of retries for commands upon server failure. + * + * -1 = unlimited retry attempts + * 0 = no retry attempts (fails immediately) + * n = fail only after n retry attempts + * + * @param int $retry Number of retry attempts. + */ + public function setRetryLimit($retry) + { + $this->retryLimit = (int) $retry; + } + + /** + * Sets the initial retry interval (milliseconds). + * + * @param int $retryInterval Milliseconds between retries. + */ + public function setRetryInterval($retryInterval) + { + $this->retryInterval = (int) $retryInterval; + } + + /** + * Returns the retry interval (milliseconds). + * + * @return int Milliseconds between retries. + */ + public function getRetryInterval() + { + return (int) $this->retryInterval; } /** @@ -104,7 +151,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable public function add(NodeConnectionInterface $connection) { $this->pool[(string) $connection] = $connection; - unset($this->slotsMap); + $this->slotmap->reset(); } /** @@ -113,10 +160,9 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable public function remove(NodeConnectionInterface $connection) { if (false !== $id = array_search($connection, $this->pool, true)) { - unset( - $this->pool[$id], - $this->slotsMap - ); + $this->slotmap->reset(); + $this->slots = array_diff($this->slots, [$connection]); + unset($this->pool[$id]); return true; } @@ -134,10 +180,9 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable public function removeById($connectionID) { if (isset($this->pool[$connectionID])) { - unset( - $this->pool[$connectionID], - $this->slotsMap - ); + $this->slotmap->reset(); + $this->slots = array_diff($this->slots, [$connectionID]); + unset($this->pool[$connectionID]); return true; } @@ -154,9 +199,9 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable * initialization have the "slots" parameter properly set accordingly to the * current cluster configuration. */ - public function buildSlotsMap() + public function buildSlotMap() { - $this->slotsMap = array(); + $this->slotmap->reset(); foreach ($this->pool as $connectionID => $connection) { $parameters = $connection->getParameters(); @@ -165,80 +210,90 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable continue; } - $slots = explode('-', $parameters->slots, 2); - $this->setSlots($slots[0], $slots[1], $connectionID); + foreach (explode(',', $parameters->slots) as $slotRange) { + $slots = explode('-', $slotRange, 2); + + if (!isset($slots[1])) { + $slots[1] = $slots[0]; + } + + $this->slotmap->setSlots($slots[0], $slots[1], $connectionID); + } } } + /** + * Queries the specified node of the cluster to fetch the updated slots map. + * + * When the connection fails, this method tries to execute the same command + * on a different connection picked at random from the pool of known nodes, + * up until the retry limit is reached. + * + * @param NodeConnectionInterface $connection Connection to a node of the cluster. + * + * @return mixed + */ + private function queryClusterNodeForSlotMap(NodeConnectionInterface $connection) + { + $retries = 0; + $retryAfter = $this->retryInterval; + $command = RawCommand::create('CLUSTER', 'SLOTS'); + + while ($retries <= $this->retryLimit) { + try { + $response = $connection->executeCommand($command); + break; + } catch (ConnectionException $exception) { + $connection = $exception->getConnection(); + $connection->disconnect(); + + $this->remove($connection); + + if ($retries === $this->retryLimit) { + throw $exception; + } + + if (!$connection = $this->getRandomConnection()) { + throw new ClientException('No connections left in the pool for `CLUSTER SLOTS`'); + } + + usleep($retryAfter * 1000); + $retryAfter = $retryAfter * 2; + ++$retries; + } + } + + return $response; + } + /** * Generates an updated slots map fetching the cluster configuration using * the CLUSTER SLOTS command against the specified node or a random one from * the pool. * * @param NodeConnectionInterface $connection Optional connection instance. - * - * @return array */ - public function askSlotsMap(NodeConnectionInterface $connection = null) + public function askSlotMap(NodeConnectionInterface $connection = null) { if (!$connection && !$connection = $this->getRandomConnection()) { - return array(); + return; } - $command = RawCommand::create('CLUSTER', 'SLOTS'); - $response = $connection->executeCommand($command); + $this->slotmap->reset(); + + $response = $this->queryClusterNodeForSlotMap($connection); foreach ($response as $slots) { // We only support master servers for now, so we ignore subsequent // elements in the $slots array identifying slaves. - list($start, $end, $master) = $slots; + [$start, $end, $master] = $slots; if ($master[0] === '') { - $this->setSlots($start, $end, (string) $connection); + $this->slotmap->setSlots($start, $end, (string) $connection); } else { - $this->setSlots($start, $end, "{$master[0]}:{$master[1]}"); + $this->slotmap->setSlots($start, $end, "{$master[0]}:{$master[1]}"); } } - - return $this->slotsMap; - } - - /** - * Returns the current slots map for the cluster. - * - * @return array - */ - public function getSlotsMap() - { - if (!isset($this->slotsMap)) { - $this->slotsMap = array(); - } - - return $this->slotsMap; - } - - /** - * Pre-associates a connection to a slots range to avoid runtime guessing. - * - * @param int $first Initial slot of the range. - * @param int $last Last slot of the range. - * @param NodeConnectionInterface|string $connection ID or connection instance. - * - * @throws \OutOfBoundsException - */ - public function setSlots($first, $last, $connection) - { - if ($first < 0x0000 || $first > 0x3FFF || - $last < 0x0000 || $last > 0x3FFF || - $last < $first - ) { - throw new \OutOfBoundsException( - "Invalid slot range for $connection: [$first-$last]." - ); - } - - $slots = array_fill($first, $last - $first + 1, (string) $connection); - $this->slotsMap = $this->getSlotsMap() + $slots; } /** @@ -252,12 +307,16 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ protected function guessNode($slot) { - if (!isset($this->slotsMap)) { - $this->buildSlotsMap(); + if (!$this->pool) { + throw new ClientException('No connections available in the pool'); } - if (isset($this->slotsMap[$slot])) { - return $this->slotsMap[$slot]; + if ($this->slotmap->isEmpty()) { + $this->buildSlotMap(); + } + + if ($node = $this->slotmap[$slot]) { + return $node; } $count = count($this->pool); @@ -278,20 +337,16 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable { $separator = strrpos($connectionID, ':'); - $parameters = array_merge($this->defaultParameters, array( + return $this->connections->create([ 'host' => substr($connectionID, 0, $separator), 'port' => substr($connectionID, $separator + 1), - )); - - $connection = $this->connections->create($parameters); - - return $connection; + ]); } /** * {@inheritdoc} */ - public function getConnection(CommandInterface $command) + public function getConnectionByCommand(CommandInterface $command) { $slot = $this->strategy->getSlot($command); @@ -313,14 +368,13 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable * * @param int $slot Slot index. * - * @throws \OutOfBoundsException - * * @return NodeConnectionInterface + * @throws OutOfBoundsException */ public function getConnectionBySlot($slot) { - if ($slot < 0x0000 || $slot > 0x3FFF) { - throw new \OutOfBoundsException("Invalid slot [$slot]."); + if (!SlotMap::isValid($slot)) { + throw new OutOfBoundsException("Invalid slot [$slot]."); } if (isset($this->slots[$slot])) { @@ -342,9 +396,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ public function getConnectionById($connectionID) { - if (isset($this->pool[$connectionID])) { - return $this->pool[$connectionID]; - } + return $this->pool[$connectionID] ?? null; } /** @@ -354,9 +406,11 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ protected function getRandomConnection() { - if ($this->pool) { - return $this->pool[array_rand($this->pool)]; + if (!$this->pool) { + return null; } + + return $this->pool[array_rand($this->pool)]; } /** @@ -370,6 +424,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable { $this->pool[(string) $connection] = $connection; $this->slots[(int) $slot] = $connection; + $this->slotmap[(int) $slot] = $connection; } /** @@ -407,20 +462,19 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ protected function onMovedResponse(CommandInterface $command, $details) { - list($slot, $connectionID) = explode(' ', $details, 2); + [$slot, $connectionID] = explode(' ', $details, 2); if (!$connection = $this->getConnectionById($connectionID)) { $connection = $this->createConnection($connectionID); } if ($this->useClusterSlots) { - $this->askSlotsMap($connection); + $this->askSlotMap($connection); } $this->move($connection, $slot); - $response = $this->executeCommand($command); - return $response; + return $this->executeCommand($command); } /** @@ -434,14 +488,73 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ protected function onAskResponse(CommandInterface $command, $details) { - list($slot, $connectionID) = explode(' ', $details, 2); + [$slot, $connectionID] = explode(' ', $details, 2); if (!$connection = $this->getConnectionById($connectionID)) { $connection = $this->createConnection($connectionID); } $connection->executeCommand(RawCommand::create('ASKING')); - $response = $connection->executeCommand($command); + + return $connection->executeCommand($command); + } + + /** + * Ensures that a command is executed one more time on connection failure. + * + * The connection to the node that generated the error is evicted from the + * pool before trying to fetch an updated slots map from another node. If + * the new slots map points to an unreachable server the client gives up and + * throws the exception as the nodes participating in the cluster may still + * have to agree that something changed in the configuration of the cluster. + * + * @param CommandInterface $command Command instance. + * @param string $method Actual method. + * + * @return mixed + */ + private function retryCommandOnFailure(CommandInterface $command, $method) + { + $retries = 0; + $retryAfter = $this->retryInterval; + + while ($retries <= $this->retryLimit) { + try { + $response = $this->getConnectionByCommand($command)->$method($command); + + if ($response instanceof ErrorResponse) { + $message = $response->getMessage(); + + if (strpos($message, 'CLUSTERDOWN') !== false) { + throw new ServerException($message); + } + } + + break; + } catch (Throwable $exception) { + usleep($retryAfter * 1000); + $retryAfter = $retryAfter * 2; + + if ($exception instanceof ConnectionException) { + $connection = $exception->getConnection(); + + if ($connection) { + $connection->disconnect(); + $this->remove($connection); + } + } + + if ($retries === $this->retryLimit) { + throw $exception; + } + + if ($this->useClusterSlots) { + $this->askSlotMap(); + } + + ++$retries; + } + } return $response; } @@ -451,7 +564,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ public function writeRequest(CommandInterface $command) { - $this->getConnection($command)->writeRequest($command); + $this->retryCommandOnFailure($command, __FUNCTION__); } /** @@ -459,7 +572,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ public function readResponse(CommandInterface $command) { - return $this->getConnection($command)->readResponse($command); + return $this->retryCommandOnFailure($command, __FUNCTION__); } /** @@ -467,8 +580,7 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable */ public function executeCommand(CommandInterface $command) { - $connection = $this->getConnection($command); - $response = $connection->executeCommand($command); + $response = $this->retryCommandOnFailure($command, __FUNCTION__); if ($response instanceof ErrorResponseInterface) { return $this->onErrorResponse($command, $response); @@ -478,19 +590,45 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable } /** - * {@inheritdoc} + * @return int */ + #[ReturnTypeWillChange] public function count() { return count($this->pool); } /** - * {@inheritdoc} + * @return Traversable */ + #[ReturnTypeWillChange] public function getIterator() { - return new \ArrayIterator(array_values($this->pool)); + if ($this->slotmap->isEmpty()) { + $this->useClusterSlots ? $this->askSlotMap() : $this->buildSlotMap(); + } + + $connections = []; + + foreach ($this->slotmap->getNodes() as $node) { + if (!$connection = $this->getConnectionById($node)) { + $this->add($connection = $this->createConnection($node)); + } + + $connections[] = $connection; + } + + return new ArrayIterator($connections); + } + + /** + * Returns the underlying slot map. + * + * @return SlotMap + */ + public function getSlotMap() + { + return $this->slotmap; } /** @@ -517,13 +655,13 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable /** * Enables automatic fetching of the current slots map from one of the nodes - * using the CLUSTER SLOTS command. This option is disabled by default but + * using the CLUSTER SLOTS command. This option is enabled by default as * asking the current slots map to Redis upon -MOVED responses may reduce * overhead by eliminating the trial-and-error nature of the node guessing * procedure, mostly when targeting many keys that would end up in a lot of * redirections. * - * The slots map can still be manually fetched using the askSlotsMap() + * The slots map can still be manually fetched using the askSlotMap() * method whether or not this option is enabled. * * @param bool $value Enable or disable the use of CLUSTER SLOTS. @@ -532,22 +670,4 @@ class RedisCluster implements ClusterInterface, \IteratorAggregate, \Countable { $this->useClusterSlots = (bool) $value; } - - /** - * Sets a default array of connection parameters to be applied when creating - * new connection instances on the fly when they are not part of the initial - * pool supplied upon cluster initialization. - * - * These parameters are not applied to connections added to the pool using - * the add() method. - * - * @param array $parameters Array of connection parameters. - */ - public function setDefaultParameters(array $parameters) - { - $this->defaultParameters = array_merge( - $this->defaultParameters, - $parameters ?: array() - ); - } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/CompositeConnectionInterface.php b/plugins/cache-redis/Predis/Connection/CompositeConnectionInterface.php similarity index 83% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/CompositeConnectionInterface.php rename to plugins/cache-redis/Predis/Connection/CompositeConnectionInterface.php index 286e082cc..22b8c5f77 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/CompositeConnectionInterface.php +++ b/plugins/cache-redis/Predis/Connection/CompositeConnectionInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,8 +15,6 @@ namespace Predis\Connection; /** * Defines a connection to communicate with a single Redis server that leverages * an external protocol processor to handle pluggable protocol handlers. - * - * @author Daniele Alessandri */ interface CompositeConnectionInterface extends NodeConnectionInterface { @@ -34,7 +33,7 @@ interface CompositeConnectionInterface extends NodeConnectionInterface /** * Reads the given number of bytes from the connection. * - * @param int $length Number of bytes to read from the connection. + * @param int $length Number of bytes to read from the connection. * * @return string */ @@ -43,7 +42,7 @@ interface CompositeConnectionInterface extends NodeConnectionInterface /** * Reads a line from the connection. * - * @param string + * @return string */ public function readLine(); } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/CompositeStreamConnection.php b/plugins/cache-redis/Predis/Connection/CompositeStreamConnection.php similarity index 91% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/CompositeStreamConnection.php rename to plugins/cache-redis/Predis/Connection/CompositeStreamConnection.php index 7a3534054..ad69cbc12 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/CompositeStreamConnection.php +++ b/plugins/cache-redis/Predis/Connection/CompositeStreamConnection.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,6 +12,7 @@ namespace Predis\Connection; +use InvalidArgumentException; use Predis\Command\CommandInterface; use Predis\Protocol\ProtocolProcessorInterface; use Predis\Protocol\Text\ProtocolProcessor as TextProtocolProcessor; @@ -18,8 +20,6 @@ use Predis\Protocol\Text\ProtocolProcessor as TextProtocolProcessor; /** * Connection abstraction to Redis servers based on PHP's stream that uses an * external protocol processor defining the protocol used for the communication. - * - * @author Daniele Alessandri */ class CompositeStreamConnection extends StreamConnection implements CompositeConnectionInterface { @@ -59,7 +59,7 @@ class CompositeStreamConnection extends StreamConnection implements CompositeCon public function readBuffer($length) { if ($length <= 0) { - throw new \InvalidArgumentException('Length parameter must be greater than 0.'); + throw new InvalidArgumentException('Length parameter must be greater than 0.'); } $value = ''; @@ -120,6 +120,6 @@ class CompositeStreamConnection extends StreamConnection implements CompositeCon */ public function __sleep() { - return array_merge(parent::__sleep(), array('protocol')); + return array_merge(parent::__sleep(), ['protocol']); } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/ConnectionException.php b/plugins/cache-redis/Predis/Connection/ConnectionException.php similarity index 78% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/ConnectionException.php rename to plugins/cache-redis/Predis/Connection/ConnectionException.php index ef2e9d73a..77e7a15a0 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/ConnectionException.php +++ b/plugins/cache-redis/Predis/Connection/ConnectionException.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,8 +16,6 @@ use Predis\CommunicationException; /** * Exception class that identifies connection-related errors. - * - * @author Daniele Alessandri */ class ConnectionException extends CommunicationException { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/ConnectionInterface.php b/plugins/cache-redis/Predis/Connection/ConnectionInterface.php similarity index 93% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/ConnectionInterface.php rename to plugins/cache-redis/Predis/Connection/ConnectionInterface.php index 11ace1b69..fc2014612 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/ConnectionInterface.php +++ b/plugins/cache-redis/Predis/Connection/ConnectionInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,8 +17,6 @@ use Predis\Command\CommandInterface; /** * Defines a connection object used to communicate with one or multiple * Redis servers. - * - * @author Daniele Alessandri */ interface ConnectionInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Factory.php b/plugins/cache-redis/Predis/Connection/Factory.php similarity index 58% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/Factory.php rename to plugins/cache-redis/Predis/Connection/Factory.php index c2e93f880..86b18c4a0 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Factory.php +++ b/plugins/cache-redis/Predis/Connection/Factory.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,21 +12,27 @@ namespace Predis\Connection; +use InvalidArgumentException; +use Predis\Client; use Predis\Command\RawCommand; +use ReflectionClass; +use UnexpectedValueException; /** * Standard connection factory for creating connections to Redis nodes. - * - * @author Daniele Alessandri */ class Factory implements FactoryInterface { - protected $schemes = array( + private $defaults = []; + + protected $schemes = [ 'tcp' => 'Predis\Connection\StreamConnection', 'unix' => 'Predis\Connection\StreamConnection', + 'tls' => 'Predis\Connection\StreamConnection', 'redis' => 'Predis\Connection\StreamConnection', + 'rediss' => 'Predis\Connection\StreamConnection', 'http' => 'Predis\Connection\WebdisConnection', - ); + ]; /** * Checks if the provided argument represents a valid connection class @@ -34,9 +41,8 @@ class Factory implements FactoryInterface * * @param mixed $initializer FQN of a connection class or a callable for lazy initialization. * - * @throws \InvalidArgumentException - * * @return mixed + * @throws InvalidArgumentException */ protected function checkInitializer($initializer) { @@ -44,10 +50,10 @@ class Factory implements FactoryInterface return $initializer; } - $class = new \ReflectionClass($initializer); + $class = new ReflectionClass($initializer); if (!$class->isSubclassOf('Predis\Connection\NodeConnectionInterface')) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'A connection initializer must be a valid connection class or a callable object.' ); } @@ -83,7 +89,7 @@ class Factory implements FactoryInterface $scheme = $parameters->scheme; if (!isset($this->schemes[$scheme])) { - throw new \InvalidArgumentException("Unknown connection scheme: '$scheme'."); + throw new InvalidArgumentException("Unknown connection scheme: '$scheme'."); } $initializer = $this->schemes[$scheme]; @@ -96,8 +102,8 @@ class Factory implements FactoryInterface } if (!$connection instanceof NodeConnectionInterface) { - throw new \UnexpectedValueException( - 'Objects returned by connection initializers must implement '. + throw new UnexpectedValueException( + 'Objects returned by connection initializers must implement ' . "'Predis\Connection\NodeConnectionInterface'." ); } @@ -106,13 +112,26 @@ class Factory implements FactoryInterface } /** - * {@inheritdoc} + * Assigns a default set of parameters applied to new connections. + * + * The set of parameters passed to create a new connection have precedence + * over the default values set for the connection factory. + * + * @param array $parameters Set of connection parameters. */ - public function aggregate(AggregateConnectionInterface $connection, array $parameters) + public function setDefaultParameters(array $parameters) { - foreach ($parameters as $node) { - $connection->add($node instanceof NodeConnectionInterface ? $node : $this->create($node)); - } + $this->defaults = $parameters; + } + + /** + * Returns the default set of parameters applied to new connections. + * + * @return array + */ + public function getDefaultParameters() + { + return $this->defaults; } /** @@ -124,7 +143,17 @@ class Factory implements FactoryInterface */ protected function createParameters($parameters) { - return Parameters::create($parameters); + if (is_string($parameters)) { + $parameters = Parameters::parse($parameters); + } else { + $parameters = $parameters ?: []; + } + + if ($this->defaults) { + $parameters += $this->defaults; + } + + return new Parameters($parameters); } /** @@ -136,15 +165,29 @@ class Factory implements FactoryInterface { $parameters = $connection->getParameters(); - if (isset($parameters->password)) { + if (isset($parameters->password) && strlen($parameters->password)) { + $cmdAuthArgs = isset($parameters->username) && strlen($parameters->username) + ? [$parameters->username, $parameters->password] + : [$parameters->password]; + $connection->addConnectCommand( - new RawCommand(array('AUTH', $parameters->password)) + new RawCommand('AUTH', $cmdAuthArgs) ); } - if (isset($parameters->database)) { + if ($parameters->client_info ?? false && !$connection instanceof RelayConnection) { $connection->addConnectCommand( - new RawCommand(array('SELECT', $parameters->database)) + new RawCommand('CLIENT', ['SETINFO', 'LIB-NAME', 'predis']) + ); + + $connection->addConnectCommand( + new RawCommand('CLIENT', ['SETINFO', 'LIB-VER', Client::VERSION]) + ); + } + + if (isset($parameters->database) && strlen($parameters->database)) { + $connection->addConnectCommand( + new RawCommand('SELECT', [$parameters->database]) ); } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/FactoryInterface.php b/plugins/cache-redis/Predis/Connection/FactoryInterface.php similarity index 69% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/FactoryInterface.php rename to plugins/cache-redis/Predis/Connection/FactoryInterface.php index 2bae0839e..24dc782a8 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/FactoryInterface.php +++ b/plugins/cache-redis/Predis/Connection/FactoryInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,8 +14,6 @@ namespace Predis\Connection; /** * Interface for classes providing a factory of connections to Redis nodes. - * - * @author Daniele Alessandri */ interface FactoryInterface { @@ -41,12 +40,4 @@ interface FactoryInterface * @return NodeConnectionInterface */ public function create($parameters); - - /** - * Aggregates single connections into an aggregate connection instance. - * - * @param AggregateConnectionInterface $aggregate Aggregate connection instance. - * @param array $parameters List of parameters for each connection. - */ - public function aggregate(AggregateConnectionInterface $aggregate, array $parameters); } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/NodeConnectionInterface.php b/plugins/cache-redis/Predis/Connection/NodeConnectionInterface.php similarity index 92% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/NodeConnectionInterface.php rename to plugins/cache-redis/Predis/Connection/NodeConnectionInterface.php index 665b862c1..713331776 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/NodeConnectionInterface.php +++ b/plugins/cache-redis/Predis/Connection/NodeConnectionInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,8 +16,6 @@ use Predis\Command\CommandInterface; /** * Defines a connection used to communicate with a single Redis node. - * - * @author Daniele Alessandri */ interface NodeConnectionInterface extends ConnectionInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Parameters.php b/plugins/cache-redis/Predis/Connection/Parameters.php similarity index 62% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/Parameters.php rename to plugins/cache-redis/Predis/Connection/Parameters.php index b7d986153..170d7e28e 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Parameters.php +++ b/plugins/cache-redis/Predis/Connection/Parameters.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,40 +12,49 @@ namespace Predis\Connection; +use InvalidArgumentException; + /** * Container for connection parameters used to initialize connections to Redis. * * {@inheritdoc} - * - * @author Daniele Alessandri */ class Parameters implements ParametersInterface { - private $parameters; - - private static $defaults = array( + protected static $defaults = [ 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379, - 'timeout' => 5.0, - ); + ]; + + /** + * Set of connection parameters already filtered + * for NULL or 0-length string values. + * + * @var array + */ + protected $parameters; /** * @param array $parameters Named array of connection parameters. */ - public function __construct(array $parameters = array()) + public function __construct(array $parameters = []) { - $this->parameters = $this->filter($parameters) + $this->getDefaults(); + $this->parameters = $this->filter($parameters + static::$defaults); } /** - * Returns some default parameters with their values. + * Filters parameters removing entries with NULL or 0-length string values. + * + * @params array $parameters Array of parameters to be filtered * * @return array */ - protected function getDefaults() + protected function filter(array $parameters) { - return self::$defaults; + return array_filter($parameters, function ($value) { + return $value !== null && $value !== ''; + }); } /** @@ -61,7 +71,7 @@ class Parameters implements ParametersInterface $parameters = static::parse($parameters); } - return new static($parameters ?: array()); + return new static($parameters ?: []); } /** @@ -73,24 +83,24 @@ class Parameters implements ParametersInterface * database number in the "path" part these values override the values of * "password" and "database" if they are present in the "query" part. * - * @link http://www.iana.org/assignments/uri-schemes/prov/redis - * @link http://www.iana.org/assignments/uri-schemes/prov/redis + * @see http://www.iana.org/assignments/uri-schemes/prov/redis + * @see http://www.iana.org/assignments/uri-schemes/prov/rediss * * @param string $uri URI string. * - * @throws \InvalidArgumentException - * * @return array + * @throws InvalidArgumentException */ public static function parse($uri) { - if (stripos($uri, 'unix') === 0) { - // Hack to support URIs for UNIX sockets with minimal effort. - $uri = str_ireplace('unix:///', 'unix://localhost/', $uri); + if (stripos($uri, 'unix://') === 0) { + // parse_url() can parse unix:/path/to/sock so we do not need the + // unix:///path/to/sock hack, we will support it anyway until 2.0. + $uri = str_ireplace('unix://', 'unix:', $uri); } if (!$parsed = parse_url($uri)) { - throw new \InvalidArgumentException("Invalid parameters URI: $uri"); + throw new InvalidArgumentException("Invalid parameters URI: $uri"); } if ( @@ -109,8 +119,17 @@ class Parameters implements ParametersInterface } if (stripos($uri, 'redis') === 0) { + if (isset($parsed['user'])) { + if (strlen($parsed['user'])) { + $parsed['username'] = $parsed['user']; + } + unset($parsed['user']); + } + if (isset($parsed['pass'])) { - $parsed['password'] = $parsed['pass']; + if (strlen($parsed['pass'])) { + $parsed['password'] = $parsed['pass']; + } unset($parsed['pass']); } @@ -129,15 +148,11 @@ class Parameters implements ParametersInterface } /** - * Validates and converts each value of the connection parameters array. - * - * @param array $parameters Connection parameters. - * - * @return array + * {@inheritdoc} */ - protected function filter(array $parameters) + public function toArray() { - return $parameters ?: array(); + return $this->parameters; } /** @@ -161,9 +176,17 @@ class Parameters implements ParametersInterface /** * {@inheritdoc} */ - public function toArray() + public function __toString() { - return $this->parameters; + if ($this->scheme === 'unix') { + return "$this->scheme:$this->path"; + } + + if (filter_var($this->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return "$this->scheme://[$this->host]:$this->port"; + } + + return "$this->scheme://$this->host:$this->port"; } /** @@ -171,6 +194,6 @@ class Parameters implements ParametersInterface */ public function __sleep() { - return array('parameters'); + return ['parameters']; } } diff --git a/plugins/cache-redis/Predis/Connection/ParametersInterface.php b/plugins/cache-redis/Predis/Connection/ParametersInterface.php new file mode 100644 index 000000000..7893ea117 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/ParametersInterface.php @@ -0,0 +1,72 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,9 +12,12 @@ namespace Predis\Connection; +use Closure; +use InvalidArgumentException; use Predis\Command\CommandInterface; use Predis\NotSupportedException; use Predis\Response\Error as ErrorResponse; +use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\Status as StatusResponse; /** @@ -36,12 +40,11 @@ use Predis\Response\Status as StatusResponse; * - host: hostname or IP address of the server. * - port: TCP port of the server. * - path: path of a UNIX domain socket when scheme is 'unix'. - * - timeout: timeout to perform the connection. + * - timeout: timeout to perform the connection (default is 5 seconds). * - read_write_timeout: timeout of read / write operations. * - * @link http://github.com/nrk/phpiredis - * - * @author Daniele Alessandri + * @see http://github.com/nrk/phpiredis + * @deprecated 2.1.2 */ class PhpiredisSocketConnection extends AbstractConnection { @@ -65,9 +68,9 @@ class PhpiredisSocketConnection extends AbstractConnection */ public function __destruct() { - phpiredis_reader_destroy($this->reader); - parent::__destruct(); + + phpiredis_reader_destroy($this->reader); } /** @@ -93,7 +96,15 @@ class PhpiredisSocketConnection extends AbstractConnection */ protected function assertParameters(ParametersInterface $parameters) { - parent::assertParameters($parameters); + switch ($parameters->scheme) { + case 'tcp': + case 'redis': + case 'unix': + break; + + default: + throw new InvalidArgumentException("Invalid scheme: '$parameters->scheme'."); + } if (isset($parameters->persistent)) { throw new NotSupportedException( @@ -132,25 +143,37 @@ class PhpiredisSocketConnection extends AbstractConnection /** * Returns the handler used by the protocol reader for inline responses. * - * @return \Closure + * @return Closure */ - private function getStatusHandler() + protected function getStatusHandler() { - return function ($payload) { - return StatusResponse::get($payload); - }; + static $statusHandler; + + if (!$statusHandler) { + $statusHandler = function ($payload) { + return StatusResponse::get($payload); + }; + } + + return $statusHandler; } /** * Returns the handler used by the protocol reader for error responses. * - * @return \Closure + * @return Closure */ protected function getErrorHandler() { - return function ($payload) { - return new ErrorResponse($payload); - }; + static $errorHandler; + + if (!$errorHandler) { + $errorHandler = function ($errorMessage) { + return new ErrorResponse($errorMessage); + }; + } + + return $errorHandler; } /** @@ -206,9 +229,7 @@ class PhpiredisSocketConnection extends AbstractConnection $protocol = SOL_TCP; } - $socket = @socket_create($domain, SOCK_STREAM, $protocol); - - if (!is_resource($socket)) { + if (false === $socket = @socket_create($domain, SOCK_STREAM, $protocol)) { $this->emitSocketError(); } @@ -241,10 +262,10 @@ class PhpiredisSocketConnection extends AbstractConnection $timeoutSec = floor($rwtimeout); $timeoutUsec = ($rwtimeout - $timeoutSec) * 1000000; - $timeout = array( + $timeout = [ 'sec' => $timeoutSec, 'usec' => $timeoutUsec, - ); + ]; if (!socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout)) { $this->emitSocketError(); @@ -263,7 +284,7 @@ class PhpiredisSocketConnection extends AbstractConnection * @param string $address IP address (DNS-resolved from hostname) * @param ParametersInterface $parameters Parameters used to initialize the connection. * - * @return string + * @return void */ private function connectWithTimeout($socket, $address, ParametersInterface $parameters) { @@ -280,9 +301,9 @@ class PhpiredisSocketConnection extends AbstractConnection socket_set_block($socket); $null = null; - $selectable = array($socket); + $selectable = [$socket]; - $timeout = (float) $parameters->timeout; + $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); $timeoutSecs = floor($timeout); $timeoutUSecs = ($timeout - $timeoutSecs) * 1000000; @@ -308,7 +329,11 @@ class PhpiredisSocketConnection extends AbstractConnection { if (parent::connect() && $this->initCommands) { foreach ($this->initCommands as $command) { - $this->executeCommand($command); + $response = $this->executeCommand($command); + + if ($response instanceof ErrorResponseInterface) { + $this->onConnectionError("`{$command->getId()}` failed: {$response->getMessage()}", 0); + } } } } @@ -319,7 +344,9 @@ class PhpiredisSocketConnection extends AbstractConnection public function disconnect() { if ($this->isConnected()) { + phpiredis_reader_reset($this->reader); socket_close($this->getResource()); + parent::disconnect(); } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/PhpiredisStreamConnection.php b/plugins/cache-redis/Predis/Connection/PhpiredisStreamConnection.php similarity index 75% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/PhpiredisStreamConnection.php rename to plugins/cache-redis/Predis/Connection/PhpiredisStreamConnection.php index beb235758..e3dbfd8ad 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/PhpiredisStreamConnection.php +++ b/plugins/cache-redis/Predis/Connection/PhpiredisStreamConnection.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,6 +12,8 @@ namespace Predis\Connection; +use Closure; +use InvalidArgumentException; use Predis\Command\CommandInterface; use Predis\NotSupportedException; use Predis\Response\Error as ErrorResponse; @@ -42,9 +45,8 @@ use Predis\Response\Status as StatusResponse; * - tcp_nodelay: enables or disables Nagle's algorithm for coalescing. * - persistent: the connection is left intact after a GC collection. * - * @link https://github.com/nrk/phpiredis - * - * @author Daniele Alessandri + * @see https://github.com/nrk/phpiredis + * @deprecated 2.1.2 */ class PhpiredisStreamConnection extends StreamConnection { @@ -67,9 +69,19 @@ class PhpiredisStreamConnection extends StreamConnection */ public function __destruct() { - phpiredis_reader_destroy($this->reader); - parent::__destruct(); + + phpiredis_reader_destroy($this->reader); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + phpiredis_reader_reset($this->reader); + + parent::disconnect(); } /** @@ -87,24 +99,34 @@ class PhpiredisStreamConnection extends StreamConnection /** * {@inheritdoc} */ - protected function tcpStreamInitializer(ParametersInterface $parameters) + protected function assertParameters(ParametersInterface $parameters) + { + switch ($parameters->scheme) { + case 'tcp': + case 'redis': + case 'unix': + break; + + case 'tls': + case 'rediss': + throw new InvalidArgumentException('SSL encryption is not supported by this connection backend.'); + default: + throw new InvalidArgumentException("Invalid scheme: '$parameters->scheme'."); + } + + return $parameters; + } + + /** + * {@inheritdoc} + */ + protected function createStreamSocket(ParametersInterface $parameters, $address, $flags) { - $uri = "tcp://[{$parameters->host}]:{$parameters->port}"; - $flags = STREAM_CLIENT_CONNECT; $socket = null; + $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); + $context = stream_context_create(['socket' => ['tcp_nodelay' => (bool) $parameters->tcp_nodelay]]); - if (isset($parameters->async_connect) && (bool) $parameters->async_connect) { - $flags |= STREAM_CLIENT_ASYNC_CONNECT; - } - - if (isset($parameters->persistent) && (bool) $parameters->persistent) { - $flags |= STREAM_CLIENT_PERSISTENT; - $uri .= strpos($path = $parameters->path, '/') === 0 ? $path : "/$path"; - } - - $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags); - - if (!$resource) { + if (!$resource = @stream_socket_client($address, $errno, $errstr, $timeout, $flags, $context)) { $this->onConnectionError(trim($errstr), $errno); } @@ -112,10 +134,10 @@ class PhpiredisStreamConnection extends StreamConnection $rwtimeout = (float) $parameters->read_write_timeout; $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; - $timeout = array( + $timeout = [ 'sec' => $timeoutSeconds = floor($rwtimeout), 'usec' => ($rwtimeout - $timeoutSeconds) * 1000000, - ); + ]; $socket = $socket ?: socket_import_stream($resource); @socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout); @@ -158,25 +180,37 @@ class PhpiredisStreamConnection extends StreamConnection /** * Returns the handler used by the protocol reader for inline responses. * - * @return \Closure + * @return Closure */ protected function getStatusHandler() { - return function ($payload) { - return StatusResponse::get($payload); - }; + static $statusHandler; + + if (!$statusHandler) { + $statusHandler = function ($payload) { + return StatusResponse::get($payload); + }; + } + + return $statusHandler; } /** * Returns the handler used by the protocol reader for error responses. * - * @return \Closure + * @return Closure */ protected function getErrorHandler() { - return function ($errorMessage) { - return new ErrorResponse($errorMessage); - }; + static $errorHandler; + + if (!$errorHandler) { + $errorHandler = function ($errorMessage) { + return new ErrorResponse($errorMessage); + }; + } + + return $errorHandler; } /** diff --git a/plugins/cache-redis/Predis/Connection/RelayConnection.php b/plugins/cache-redis/Predis/Connection/RelayConnection.php new file mode 100644 index 000000000..4ff674f5f --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/RelayConnection.php @@ -0,0 +1,337 @@ +assertExtensions(); + + $this->parameters = $this->assertParameters($parameters); + $this->client = $this->createClient(); + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + return $this->client->isConnected(); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + if ($this->client->isConnected()) { + $this->client->close(); + } + } + + /** + * Checks if the Relay extension is loaded in PHP. + */ + private function assertExtensions() + { + if (!extension_loaded('relay')) { + throw new NotSupportedException( + 'The "relay" extension is required by this connection backend.' + ); + } + } + + /** + * {@inheritdoc} + */ + protected function assertParameters(ParametersInterface $parameters) + { + if (!in_array($parameters->scheme, ['tcp', 'tls', 'unix', 'redis', 'rediss'])) { + throw new InvalidArgumentException("Invalid scheme: '{$parameters->scheme}'."); + } + + if (!in_array($parameters->serializer, [null, 'php', 'igbinary', 'msgpack', 'json'])) { + throw new InvalidArgumentException("Invalid serializer: '{$parameters->serializer}'."); + } + + if (!in_array($parameters->compression, [null, 'lzf', 'lz4', 'zstd'])) { + throw new InvalidArgumentException("Invalid compression algorithm: '{$parameters->compression}'."); + } + + return $parameters; + } + + /** + * Creates a new instance of the client. + * + * @return \Relay\Relay + */ + private function createClient() + { + $client = new Relay(); + + // throw when errors occur and return `null` for non-existent keys + $client->setOption(Relay::OPT_PHPREDIS_COMPATIBILITY, false); + + // use reply literals + $client->setOption(Relay::OPT_REPLY_LITERAL, true); + + // disable Relay's command/connection retry + $client->setOption(Relay::OPT_MAX_RETRIES, 0); + + // whether to use in-memory caching + $client->setOption(Relay::OPT_USE_CACHE, $this->parameters->cache ?? true); + + // set data serializer + $client->setOption(Relay::OPT_SERIALIZER, constant(sprintf( + '%s::SERIALIZER_%s', + Relay::class, + strtoupper($this->parameters->serializer ?? 'none') + ))); + + // set data compression algorithm + $client->setOption(Relay::OPT_COMPRESSION, constant(sprintf( + '%s::COMPRESSION_%s', + Relay::class, + strtoupper($this->parameters->compression ?? 'none') + ))); + + return $client; + } + + /** + * Returns the underlying client. + * + * @return \Relay\Relay + */ + public function getClient() + { + return $this->client; + } + + /** + * {@inheritdoc} + */ + protected function getIdentifier() + { + return $this->client->endpointId(); + } + + /** + * {@inheritdoc} + */ + protected function createStreamSocket(ParametersInterface $parameters, $address, $flags) + { + $timeout = isset($parameters->timeout) ? (float) $parameters->timeout : 5.0; + + $retry_interval = 0; + $read_timeout = 5.0; + + if (isset($parameters->read_write_timeout)) { + $read_timeout = (float) $parameters->read_write_timeout; + $read_timeout = $read_timeout > 0 ? $read_timeout : 0; + } + + try { + $this->client->connect( + $parameters->path ?? $parameters->host, + isset($parameters->path) ? 0 : $parameters->port, + $timeout, + null, + $retry_interval, + $read_timeout + ); + } catch (RelayException $ex) { + $this->onConnectionError($ex->getMessage(), $ex->getCode()); + } + + return $this->client; + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + if (!$this->client->isConnected()) { + $this->getResource(); + } + + try { + $name = $command->getId(); + + // When using compression or a serializer, we'll need a dedicated + // handler for `Predis\Command\RawCommand` calls, currently both + // parameters are unsupported until a future Relay release + return in_array($name, $this->atypicalCommands) + ? $this->client->{$name}(...$command->getArguments()) + : $this->client->rawCommand($name, ...$command->getArguments()); + } catch (RelayException $ex) { + throw $this->onCommandError($ex, $command); + } + } + + /** + * {@inheritdoc} + */ + public function onCommandError(RelayException $exception, CommandInterface $command) + { + $code = $exception->getCode(); + $message = $exception->getMessage(); + + if (strpos($message, 'RELAY_ERR_IO')) { + return new ConnectionException($this, $message, $code, $exception); + } + + if (strpos($message, 'RELAY_ERR_REDIS')) { + return new ServerException($message, $code, $exception); + } + + if (strpos($message, 'RELAY_ERR_WRONGTYPE') && strpos($message, "Got reply-type 'status'")) { + $message = 'Operation against a key holding the wrong kind of value'; + } + + return new ClientException($message, $code, $exception); + } + + /** + * Applies the configured serializer and compression to given value. + * + * @param mixed $value + * @return string + */ + public function pack($value) + { + return $this->client->_pack($value); + } + + /** + * Deserializes and decompresses to given value. + * + * @param mixed $value + * @return string + */ + public function unpack($value) + { + return $this->client->_unpack($value); + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + throw new NotSupportedException('The "relay" extension does not support writing requests.'); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + throw new NotSupportedException('The "relay" extension does not support reading responses.'); + } + + /** + * {@inheritdoc} + */ + public function __destruct() + { + $this->disconnect(); + } + + /** + * {@inheritdoc} + */ + public function __wakeup() + { + $this->assertExtensions(); + $this->client = $this->createClient(); + } +} diff --git a/plugins/cache-redis/Predis/Connection/RelayMethods.php b/plugins/cache-redis/Predis/Connection/RelayMethods.php new file mode 100644 index 000000000..a52c4a035 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/RelayMethods.php @@ -0,0 +1,136 @@ +client->onFlushed($callback); + } + + /** + * Registers a new `invalidated` event listener. + * + * @param callable $callback + * @param string $pattern + * @return bool + */ + public function onInvalidated(?callable $callback, string $pattern = null) + { + return $this->client->onInvalidated($callback, $pattern); + } + + /** + * Dispatches all pending events. + * + * @return int|false + */ + public function dispatchEvents() + { + return $this->client->dispatchEvents(); + } + + /** + * Adds ignore pattern(s). Matching keys will not be cached in memory. + * + * @param string $pattern,... + * @return int + */ + public function addIgnorePatterns(string ...$pattern) + { + return $this->client->addIgnorePatterns(...$pattern); + } + + /** + * Adds allow pattern(s). Only matching keys will be cached in memory. + * + * @param string $pattern,... + * @return int + */ + public function addAllowPatterns(string ...$pattern) + { + return $this->client->addAllowPatterns(...$pattern); + } + + /** + * Returns the connection's endpoint identifier. + * + * @return string|false + */ + public function endpointId() + { + return $this->client->endpointId(); + } + + /** + * Returns a unique representation of the underlying socket connection identifier. + * + * @return string|false + */ + public function socketId() + { + return $this->client->socketId(); + } + + /** + * Returns information about the license. + * + * @return array + */ + public function license() + { + return $this->client->license(); + } + + /** + * Returns statistics about Relay. + * + * @return array> + */ + public function stats() + { + return $this->client->stats(); + } + + /** + * Returns the number of bytes allocated, or `0` in client-only mode. + * + * @return int + */ + public function maxMemory() + { + return $this->client->maxMemory(); + } + + /** + * Flushes Relay's in-memory cache of all databases. + * When given an endpoint, only that connection will be flushed. + * When given an endpoint and database index, only that database + * for that connection will be flushed. + * + * @param ?string $endpointId + * @param ?int $db + * @return bool + */ + public function flushMemory(string $endpointId = null, int $db = null) + { + return $this->client->flushMemory($endpointId, $db); + } +} diff --git a/plugins/cache-redis/Predis/Connection/Replication/MasterSlaveReplication.php b/plugins/cache-redis/Predis/Connection/Replication/MasterSlaveReplication.php new file mode 100644 index 000000000..9d57a5ac3 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Replication/MasterSlaveReplication.php @@ -0,0 +1,553 @@ +strategy = $strategy ?: new ReplicationStrategy(); + } + + /** + * Configures the automatic discovery of the replication configuration on failure. + * + * @param bool $value Enable or disable auto discovery. + */ + public function setAutoDiscovery($value) + { + if (!$this->connectionFactory) { + throw new ClientException('Automatic discovery requires a connection factory'); + } + + $this->autoDiscovery = (bool) $value; + } + + /** + * Sets the connection factory used to create the connections by the auto + * discovery procedure. + * + * @param FactoryInterface $connectionFactory Connection factory instance. + */ + public function setConnectionFactory(FactoryInterface $connectionFactory) + { + $this->connectionFactory = $connectionFactory; + } + + /** + * Resets the connection state. + */ + protected function reset() + { + $this->current = null; + } + + /** + * {@inheritdoc} + */ + public function add(NodeConnectionInterface $connection) + { + $parameters = $connection->getParameters(); + + if ('master' === $parameters->role) { + $this->master = $connection; + } else { + // everything else is considered a slvave. + $this->slaves[] = $connection; + } + + if (isset($parameters->alias)) { + $this->aliases[$parameters->alias] = $connection; + } + + $this->pool[(string) $connection] = $connection; + + $this->reset(); + } + + /** + * {@inheritdoc} + */ + public function remove(NodeConnectionInterface $connection) + { + if ($connection === $this->master) { + $this->master = null; + } elseif (false !== $id = array_search($connection, $this->slaves, true)) { + unset($this->slaves[$id]); + } else { + return false; + } + + unset($this->pool[(string) $connection]); + + if ($this->aliases && $alias = $connection->getParameters()->alias) { + unset($this->aliases[$alias]); + } + + $this->reset(); + + return true; + } + + /** + * {@inheritdoc} + */ + public function getConnectionByCommand(CommandInterface $command) + { + if (!$this->current) { + if ($this->strategy->isReadOperation($command) && $slave = $this->pickSlave()) { + $this->current = $slave; + } else { + $this->current = $this->getMasterOrDie(); + } + + return $this->current; + } + + if ($this->current === $master = $this->getMasterOrDie()) { + return $master; + } + + if (!$this->strategy->isReadOperation($command) || !$this->slaves) { + $this->current = $master; + } + + return $this->current; + } + + /** + * {@inheritdoc} + */ + public function getConnectionById($id) + { + return $this->pool[$id] ?? null; + } + + /** + * Returns a connection instance by its alias. + * + * @param string $alias Connection alias. + * + * @return NodeConnectionInterface|null + */ + public function getConnectionByAlias($alias) + { + return $this->aliases[$alias] ?? null; + } + + /** + * Returns a connection by its role. + * + * @param string $role Connection role (`master` or `slave`) + * + * @return NodeConnectionInterface|null + */ + public function getConnectionByRole($role) + { + if ($role === 'master') { + return $this->getMaster(); + } elseif ($role === 'slave') { + return $this->pickSlave(); + } + + return null; + } + + /** + * Switches the internal connection in use by the backend. + * + * @param NodeConnectionInterface $connection Connection instance in the pool. + */ + public function switchTo(NodeConnectionInterface $connection) + { + if ($connection && $connection === $this->current) { + return; + } + + if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) { + throw new InvalidArgumentException('Invalid connection or connection not found.'); + } + + $this->current = $connection; + } + + /** + * {@inheritdoc} + */ + public function switchToMaster() + { + if (!$connection = $this->getConnectionByRole('master')) { + throw new InvalidArgumentException('Invalid connection or connection not found.'); + } + + $this->switchTo($connection); + } + + /** + * {@inheritdoc} + */ + public function switchToSlave() + { + if (!$connection = $this->getConnectionByRole('slave')) { + throw new InvalidArgumentException('Invalid connection or connection not found.'); + } + + $this->switchTo($connection); + } + + /** + * {@inheritdoc} + */ + public function getCurrent() + { + return $this->current; + } + + /** + * {@inheritdoc} + */ + public function getMaster() + { + return $this->master; + } + + /** + * Returns the connection associated to the master server. + * + * @return NodeConnectionInterface + */ + private function getMasterOrDie() + { + if (!$connection = $this->getMaster()) { + throw new MissingMasterException('No master server available for replication'); + } + + return $connection; + } + + /** + * {@inheritdoc} + */ + public function getSlaves() + { + return $this->slaves; + } + + /** + * Returns the underlying replication strategy. + * + * @return ReplicationStrategy + */ + public function getReplicationStrategy() + { + return $this->strategy; + } + + /** + * Returns a random slave. + * + * @return NodeConnectionInterface|null + */ + protected function pickSlave() + { + if (!$this->slaves) { + return null; + } + + return $this->slaves[array_rand($this->slaves)]; + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + return $this->current ? $this->current->isConnected() : false; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if (!$this->current) { + if (!$this->current = $this->pickSlave()) { + if (!$this->current = $this->getMaster()) { + throw new ClientException('No available connection for replication'); + } + } + } + + $this->current->connect(); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + foreach ($this->pool as $connection) { + $connection->disconnect(); + } + } + + /** + * Handles response from INFO. + * + * @param string $response + * + * @return array + */ + private function handleInfoResponse($response) + { + $info = []; + + foreach (preg_split('/\r?\n/', $response) as $row) { + if (strpos($row, ':') === false) { + continue; + } + + [$k, $v] = explode(':', $row, 2); + $info[$k] = $v; + } + + return $info; + } + + /** + * Fetches the replication configuration from one of the servers. + */ + public function discover() + { + if (!$this->connectionFactory) { + throw new ClientException('Discovery requires a connection factory'); + } + + while (true) { + try { + if ($connection = $this->getMaster()) { + $this->discoverFromMaster($connection, $this->connectionFactory); + break; + } elseif ($connection = $this->pickSlave()) { + $this->discoverFromSlave($connection, $this->connectionFactory); + break; + } else { + throw new ClientException('No connection available for discovery'); + } + } catch (ConnectionException $exception) { + $this->remove($connection); + } + } + } + + /** + * Discovers the replication configuration by contacting the master node. + * + * @param NodeConnectionInterface $connection Connection to the master node. + * @param FactoryInterface $connectionFactory Connection factory instance. + */ + protected function discoverFromMaster(NodeConnectionInterface $connection, FactoryInterface $connectionFactory) + { + $response = $connection->executeCommand(RawCommand::create('INFO', 'REPLICATION')); + $replication = $this->handleInfoResponse($response); + + if ($replication['role'] !== 'master') { + throw new ClientException("Role mismatch (expected master, got slave) [$connection]"); + } + + $this->slaves = []; + + foreach ($replication as $k => $v) { + $parameters = null; + + if (strpos($k, 'slave') === 0 && preg_match('/ip=(?P.*),port=(?P\d+)/', $v, $parameters)) { + $slaveConnection = $connectionFactory->create([ + 'host' => $parameters['host'], + 'port' => $parameters['port'], + 'role' => 'slave', + ]); + + $this->add($slaveConnection); + } + } + } + + /** + * Discovers the replication configuration by contacting one of the slaves. + * + * @param NodeConnectionInterface $connection Connection to one of the slaves. + * @param FactoryInterface $connectionFactory Connection factory instance. + */ + protected function discoverFromSlave(NodeConnectionInterface $connection, FactoryInterface $connectionFactory) + { + $response = $connection->executeCommand(RawCommand::create('INFO', 'REPLICATION')); + $replication = $this->handleInfoResponse($response); + + if ($replication['role'] !== 'slave') { + throw new ClientException("Role mismatch (expected slave, got master) [$connection]"); + } + + $masterConnection = $connectionFactory->create([ + 'host' => $replication['master_host'], + 'port' => $replication['master_port'], + 'role' => 'master', + ]); + + $this->add($masterConnection); + + $this->discoverFromMaster($masterConnection, $connectionFactory); + } + + /** + * Retries the execution of a command upon slave failure. + * + * @param CommandInterface $command Command instance. + * @param string $method Actual method. + * + * @return mixed + */ + private function retryCommandOnFailure(CommandInterface $command, $method) + { + while (true) { + try { + $connection = $this->getConnectionByCommand($command); + $response = $connection->$method($command); + + if ($response instanceof ResponseErrorInterface && $response->getErrorType() === 'LOADING') { + throw new ConnectionException($connection, "Redis is loading the dataset in memory [$connection]"); + } + + break; + } catch (ConnectionException $exception) { + $connection = $exception->getConnection(); + $connection->disconnect(); + + if ($connection === $this->master && !$this->autoDiscovery) { + // Throw immediately when master connection is failing, even + // when the command represents a read-only operation, unless + // automatic discovery has been enabled. + throw $exception; + } else { + // Otherwise remove the failing slave and attempt to execute + // the command again on one of the remaining slaves... + $this->remove($connection); + } + + // ... that is, unless we have no more connections to use. + if (!$this->slaves && !$this->master) { + throw $exception; + } elseif ($this->autoDiscovery) { + $this->discover(); + } + } catch (MissingMasterException $exception) { + if ($this->autoDiscovery) { + $this->discover(); + } else { + throw $exception; + } + } + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function __sleep() + { + return ['master', 'slaves', 'pool', 'aliases', 'strategy']; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/ReplicationInterface.php b/plugins/cache-redis/Predis/Connection/Replication/ReplicationInterface.php similarity index 55% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/ReplicationInterface.php rename to plugins/cache-redis/Predis/Connection/Replication/ReplicationInterface.php index e09e8265c..14fd2499e 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/Aggregate/ReplicationInterface.php +++ b/plugins/cache-redis/Predis/Connection/Replication/ReplicationInterface.php @@ -3,50 +3,51 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Predis\Connection\Aggregate; +namespace Predis\Connection\Replication; use Predis\Connection\AggregateConnectionInterface; use Predis\Connection\NodeConnectionInterface; /** * Defines a group of Redis nodes in a master / slave replication setup. - * - * @author Daniele Alessandri */ interface ReplicationInterface extends AggregateConnectionInterface { /** - * Switches the internal connection instance in use. - * - * @param string $connection Alias of a connection + * Switches the internal connection in use to the master server. */ - public function switchTo($connection); + public function switchToMaster(); /** - * Returns the connection instance currently in use by the aggregate - * connection. + * Switches the internal connection in use to a random slave server. + */ + public function switchToSlave(); + + /** + * Returns the connection in use by the replication backend. * * @return NodeConnectionInterface */ public function getCurrent(); /** - * Returns the connection instance for the master Redis node. + * Returns the connection to the master server. * * @return NodeConnectionInterface */ public function getMaster(); /** - * Returns a list of connection instances to slave nodes. + * Returns a list of connections to slave servers. * - * @return NodeConnectionInterface + * @return NodeConnectionInterface[] */ public function getSlaves(); } diff --git a/plugins/cache-redis/Predis/Connection/Replication/SentinelReplication.php b/plugins/cache-redis/Predis/Connection/Replication/SentinelReplication.php new file mode 100644 index 000000000..14dbe2758 --- /dev/null +++ b/plugins/cache-redis/Predis/Connection/Replication/SentinelReplication.php @@ -0,0 +1,775 @@ + + * @author Ville Mattila + */ +class SentinelReplication implements ReplicationInterface +{ + /** + * @var NodeConnectionInterface + */ + protected $master; + + /** + * @var NodeConnectionInterface[] + */ + protected $slaves = []; + + /** + * @var NodeConnectionInterface[] + */ + protected $pool = []; + + /** + * @var NodeConnectionInterface + */ + protected $current; + + /** + * @var string + */ + protected $service; + + /** + * @var ConnectionFactoryInterface + */ + protected $connectionFactory; + + /** + * @var ReplicationStrategy + */ + protected $strategy; + + /** + * @var NodeConnectionInterface[] + */ + protected $sentinels = []; + + /** + * @var int + */ + protected $sentinelIndex = 0; + + /** + * @var NodeConnectionInterface + */ + protected $sentinelConnection; + + /** + * @var float + */ + protected $sentinelTimeout = 0.100; + + /** + * Max number of automatic retries of commands upon server failure. + * + * -1 = unlimited retry attempts + * 0 = no retry attempts (fails immediately) + * n = fail only after n retry attempts + * + * @var int + */ + protected $retryLimit = 20; + + /** + * Time to wait in milliseconds before fetching a new configuration from one + * of the sentinel servers. + * + * @var int + */ + protected $retryWait = 1000; + + /** + * Flag for automatic fetching of available sentinels. + * + * @var bool + */ + protected $updateSentinels = false; + + /** + * @param string $service Name of the service for autodiscovery. + * @param array $sentinels Sentinel servers connection parameters. + * @param ConnectionFactoryInterface $connectionFactory Connection factory instance. + * @param ReplicationStrategy $strategy Replication strategy instance. + */ + public function __construct( + $service, + array $sentinels, + ConnectionFactoryInterface $connectionFactory, + ReplicationStrategy $strategy = null + ) { + $this->sentinels = $sentinels; + $this->service = $service; + $this->connectionFactory = $connectionFactory; + $this->strategy = $strategy ?: new ReplicationStrategy(); + } + + /** + * Sets a default timeout for connections to sentinels. + * + * When "timeout" is present in the connection parameters of sentinels, its + * value overrides the default sentinel timeout. + * + * @param float $timeout Timeout value. + */ + public function setSentinelTimeout($timeout) + { + $this->sentinelTimeout = (float) $timeout; + } + + /** + * Sets the maximum number of retries for commands upon server failure. + * + * -1 = unlimited retry attempts + * 0 = no retry attempts (fails immediately) + * n = fail only after n retry attempts + * + * @param int $retry Number of retry attempts. + */ + public function setRetryLimit($retry) + { + $this->retryLimit = (int) $retry; + } + + /** + * Sets the time to wait (in milliseconds) before fetching a new configuration + * from one of the sentinels. + * + * @param float $milliseconds Time to wait before the next attempt. + */ + public function setRetryWait($milliseconds) + { + $this->retryWait = (float) $milliseconds; + } + + /** + * Set automatic fetching of available sentinels. + * + * @param bool $update Enable or disable automatic updates. + */ + public function setUpdateSentinels($update) + { + $this->updateSentinels = (bool) $update; + } + + /** + * Resets the current connection. + */ + protected function reset() + { + $this->current = null; + } + + /** + * Wipes the current list of master and slaves nodes. + */ + protected function wipeServerList() + { + $this->reset(); + + $this->master = null; + $this->slaves = []; + $this->pool = []; + } + + /** + * {@inheritdoc} + */ + public function add(NodeConnectionInterface $connection) + { + $parameters = $connection->getParameters(); + $role = $parameters->role; + + if ('master' === $role) { + $this->master = $connection; + } elseif ('sentinel' === $role) { + $this->sentinels[] = $connection; + + // sentinels are not considered part of the pool. + return; + } else { + // everything else is considered a slave. + $this->slaves[] = $connection; + } + + $this->pool[(string) $connection] = $connection; + + $this->reset(); + } + + /** + * {@inheritdoc} + */ + public function remove(NodeConnectionInterface $connection) + { + if ($connection === $this->master) { + $this->master = null; + } elseif (false !== $id = array_search($connection, $this->slaves, true)) { + unset($this->slaves[$id]); + } elseif (false !== $id = array_search($connection, $this->sentinels, true)) { + unset($this->sentinels[$id]); + + return true; + } else { + return false; + } + + unset($this->pool[(string) $connection]); + + $this->reset(); + + return true; + } + + /** + * Creates a new connection to a sentinel server. + * + * @return NodeConnectionInterface + */ + protected function createSentinelConnection($parameters) + { + if ($parameters instanceof NodeConnectionInterface) { + return $parameters; + } + + if (is_string($parameters)) { + $parameters = Parameters::parse($parameters); + } + + if (is_array($parameters)) { + // NOTE: sentinels do not accept AUTH and SELECT commands so we must + // explicitly set them to NULL to avoid problems when using default + // parameters set via client options. Actually AUTH is supported for + // sentinels starting with Redis 5 but we have to differentiate from + // sentinels passwords and nodes passwords, this will be implemented + // in a later release. + $parameters['database'] = null; + $parameters['username'] = null; + + // don't leak password from between configurations + // https://github.com/predis/predis/pull/807/#discussion_r985764770 + if (!isset($parameters['password'])) { + $parameters['password'] = null; + } + + if (!isset($parameters['timeout'])) { + $parameters['timeout'] = $this->sentinelTimeout; + } + } + + return $this->connectionFactory->create($parameters); + } + + /** + * Returns the current sentinel connection. + * + * If there is no active sentinel connection, a new connection is created. + * + * @return NodeConnectionInterface + */ + public function getSentinelConnection() + { + if (!$this->sentinelConnection) { + if ($this->sentinelIndex >= count($this->sentinels)) { + $this->sentinelIndex = 0; + throw new \Predis\ClientException('No sentinel server available for autodiscovery.'); + } + + $sentinel = $this->sentinels[$this->sentinelIndex]; + ++$this->sentinelIndex; + $this->sentinelConnection = $this->createSentinelConnection($sentinel); + } + + return $this->sentinelConnection; + } + + /** + * Fetches an updated list of sentinels from a sentinel. + */ + public function updateSentinels() + { + SENTINEL_QUERY: { + $sentinel = $this->getSentinelConnection(); + + try { + $payload = $sentinel->executeCommand( + RawCommand::create('SENTINEL', 'sentinels', $this->service) + ); + + $this->sentinels = []; + $this->sentinelIndex = 0; + // NOTE: sentinel server does not return itself, so we add it back. + $this->sentinels[] = $sentinel->getParameters()->toArray(); + + foreach ($payload as $sentinel) { + $this->sentinels[] = [ + 'host' => $sentinel[3], + 'port' => $sentinel[5], + 'role' => 'sentinel', + ]; + } + } catch (ConnectionException $exception) { + $this->sentinelConnection = null; + + goto SENTINEL_QUERY; + } + } + } + + /** + * Fetches the details for the master and slave servers from a sentinel. + */ + public function querySentinel() + { + $this->wipeServerList(); + + $this->updateSentinels(); + $this->getMaster(); + $this->getSlaves(); + } + + /** + * Handles error responses returned by redis-sentinel. + * + * @param NodeConnectionInterface $sentinel Connection to a sentinel server. + * @param ErrorResponseInterface $error Error response. + */ + private function handleSentinelErrorResponse(NodeConnectionInterface $sentinel, ErrorResponseInterface $error) + { + if ($error->getErrorType() === 'IDONTKNOW') { + throw new ConnectionException($sentinel, $error->getMessage()); + } else { + throw new ServerException($error->getMessage()); + } + } + + /** + * Fetches the details for the master server from a sentinel. + * + * @param NodeConnectionInterface $sentinel Connection to a sentinel server. + * @param string $service Name of the service. + * + * @return array + */ + protected function querySentinelForMaster(NodeConnectionInterface $sentinel, $service) + { + $payload = $sentinel->executeCommand( + RawCommand::create('SENTINEL', 'get-master-addr-by-name', $service) + ); + + if ($payload === null) { + throw new ServerException('ERR No such master with that name'); + } + + if ($payload instanceof ErrorResponseInterface) { + $this->handleSentinelErrorResponse($sentinel, $payload); + } + + return [ + 'host' => $payload[0], + 'port' => $payload[1], + 'role' => 'master', + ]; + } + + /** + * Fetches the details for the slave servers from a sentinel. + * + * @param NodeConnectionInterface $sentinel Connection to a sentinel server. + * @param string $service Name of the service. + * + * @return array + */ + protected function querySentinelForSlaves(NodeConnectionInterface $sentinel, $service) + { + $slaves = []; + + $payload = $sentinel->executeCommand( + RawCommand::create('SENTINEL', 'slaves', $service) + ); + + if ($payload instanceof ErrorResponseInterface) { + $this->handleSentinelErrorResponse($sentinel, $payload); + } + + foreach ($payload as $slave) { + $flags = explode(',', $slave[9]); + + if (array_intersect($flags, ['s_down', 'o_down', 'disconnected'])) { + continue; + } + + $slaves[] = [ + 'host' => $slave[3], + 'port' => $slave[5], + 'role' => 'slave', + ]; + } + + return $slaves; + } + + /** + * {@inheritdoc} + */ + public function getCurrent() + { + return $this->current; + } + + /** + * {@inheritdoc} + */ + public function getMaster() + { + if ($this->master) { + return $this->master; + } + + if ($this->updateSentinels) { + $this->updateSentinels(); + } + + SENTINEL_QUERY: { + $sentinel = $this->getSentinelConnection(); + + try { + $masterParameters = $this->querySentinelForMaster($sentinel, $this->service); + $masterConnection = $this->connectionFactory->create($masterParameters); + + $this->add($masterConnection); + } catch (ConnectionException $exception) { + $this->sentinelConnection = null; + + goto SENTINEL_QUERY; + } + } + + return $masterConnection; + } + + /** + * {@inheritdoc} + */ + public function getSlaves() + { + if ($this->slaves) { + return array_values($this->slaves); + } + + if ($this->updateSentinels) { + $this->updateSentinels(); + } + + SENTINEL_QUERY: { + $sentinel = $this->getSentinelConnection(); + + try { + $slavesParameters = $this->querySentinelForSlaves($sentinel, $this->service); + + foreach ($slavesParameters as $slaveParameters) { + $this->add($this->connectionFactory->create($slaveParameters)); + } + } catch (ConnectionException $exception) { + $this->sentinelConnection = null; + + goto SENTINEL_QUERY; + } + } + + return array_values($this->slaves); + } + + /** + * Returns a random slave. + * + * @return NodeConnectionInterface|null + */ + protected function pickSlave() + { + $slaves = $this->getSlaves(); + + return $slaves + ? $slaves[rand(1, count($slaves)) - 1] + : null; + } + + /** + * Returns the connection instance in charge for the given command. + * + * @param CommandInterface $command Command instance. + * + * @return NodeConnectionInterface + */ + private function getConnectionInternal(CommandInterface $command) + { + if (!$this->current) { + if ($this->strategy->isReadOperation($command) && $slave = $this->pickSlave()) { + $this->current = $slave; + } else { + $this->current = $this->getMaster(); + } + + return $this->current; + } + + if ($this->current === $this->master) { + return $this->current; + } + + if (!$this->strategy->isReadOperation($command)) { + $this->current = $this->getMaster(); + } + + return $this->current; + } + + /** + * Asserts that the specified connection matches an expected role. + * + * @param NodeConnectionInterface $connection Connection to a redis server. + * @param string $role Expected role of the server ("master", "slave" or "sentinel"). + * + * @throws RoleException|ConnectionException + */ + protected function assertConnectionRole(NodeConnectionInterface $connection, $role) + { + $role = strtolower($role); + $actualRole = $connection->executeCommand(RawCommand::create('ROLE')); + + if ($actualRole instanceof Error) { + throw new ConnectionException($connection, $actualRole->getMessage()); + } + + if ($role !== $actualRole[0]) { + throw new RoleException($connection, "Expected $role but got $actualRole[0] [$connection]"); + } + } + + /** + * {@inheritdoc} + */ + public function getConnectionByCommand(CommandInterface $command) + { + $connection = $this->getConnectionInternal($command); + + if (!$connection->isConnected()) { + // When we do not have any available slave in the pool we can expect + // read-only operations to hit the master server. + $expectedRole = $this->strategy->isReadOperation($command) && $this->slaves ? 'slave' : 'master'; + $this->assertConnectionRole($connection, $expectedRole); + } + + return $connection; + } + + /** + * {@inheritdoc} + */ + public function getConnectionById($id) + { + return $this->pool[$id] ?? null; + } + + /** + * Returns a connection by its role. + * + * @param string $role Connection role (`master`, `slave` or `sentinel`) + * + * @return NodeConnectionInterface|null + */ + public function getConnectionByRole($role) + { + if ($role === 'master') { + return $this->getMaster(); + } elseif ($role === 'slave') { + return $this->pickSlave(); + } elseif ($role === 'sentinel') { + return $this->getSentinelConnection(); + } else { + return null; + } + } + + /** + * Switches the internal connection in use by the backend. + * + * Sentinel connections are not considered as part of the pool, meaning that + * trying to switch to a sentinel will throw an exception. + * + * @param NodeConnectionInterface $connection Connection instance in the pool. + */ + public function switchTo(NodeConnectionInterface $connection) + { + if ($connection && $connection === $this->current) { + return; + } + + if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) { + throw new InvalidArgumentException('Invalid connection or connection not found.'); + } + + $connection->connect(); + + if ($this->current) { + $this->current->disconnect(); + } + + $this->current = $connection; + } + + /** + * {@inheritdoc} + */ + public function switchToMaster() + { + $connection = $this->getConnectionByRole('master'); + $this->switchTo($connection); + } + + /** + * {@inheritdoc} + */ + public function switchToSlave() + { + $connection = $this->getConnectionByRole('slave'); + $this->switchTo($connection); + } + + /** + * {@inheritdoc} + */ + public function isConnected() + { + return $this->current ? $this->current->isConnected() : false; + } + + /** + * {@inheritdoc} + */ + public function connect() + { + if (!$this->current) { + if (!$this->current = $this->pickSlave()) { + $this->current = $this->getMaster(); + } + } + + $this->current->connect(); + } + + /** + * {@inheritdoc} + */ + public function disconnect() + { + foreach ($this->pool as $connection) { + $connection->disconnect(); + } + } + + /** + * Retries the execution of a command upon server failure after asking a new + * configuration to one of the sentinels. + * + * @param CommandInterface $command Command instance. + * @param string $method Actual method. + * + * @return mixed + */ + private function retryCommandOnFailure(CommandInterface $command, $method) + { + $retries = 0; + + while ($retries <= $this->retryLimit) { + try { + $response = $this->getConnectionByCommand($command)->$method($command); + break; + } catch (CommunicationException $exception) { + $this->wipeServerList(); + $exception->getConnection()->disconnect(); + + if ($retries === $this->retryLimit) { + throw $exception; + } + + usleep($this->retryWait * 1000); + + ++$retries; + } + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function writeRequest(CommandInterface $command) + { + $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function readResponse(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * {@inheritdoc} + */ + public function executeCommand(CommandInterface $command) + { + return $this->retryCommandOnFailure($command, __FUNCTION__); + } + + /** + * Returns the underlying replication strategy. + * + * @return ReplicationStrategy + */ + public function getReplicationStrategy() + { + return $this->strategy; + } + + /** + * {@inheritdoc} + */ + public function __sleep() + { + return [ + 'master', 'slaves', 'pool', 'service', 'sentinels', 'connectionFactory', 'strategy', + ]; + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/StreamConnection.php b/plugins/cache-redis/Predis/Connection/StreamConnection.php similarity index 54% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/StreamConnection.php rename to plugins/cache-redis/Predis/Connection/StreamConnection.php index ed6540b87..2fe307067 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/StreamConnection.php +++ b/plugins/cache-redis/Predis/Connection/StreamConnection.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,25 +12,26 @@ namespace Predis\Connection; +use InvalidArgumentException; use Predis\Command\CommandInterface; use Predis\Response\Error as ErrorResponse; +use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\Status as StatusResponse; /** * Standard connection to Redis servers implemented on top of PHP's streams. * The connection parameters supported by this class are:. * - * - scheme: it can be either 'redis', 'tcp' or 'unix'. + * - scheme: it can be either 'redis', 'tcp', 'rediss', 'tls' or 'unix'. * - host: hostname or IP address of the server. * - port: TCP port of the server. * - path: path of a UNIX domain socket when scheme is 'unix'. - * - timeout: timeout to perform the connection. + * - timeout: timeout to perform the connection (default is 5 seconds). * - read_write_timeout: timeout of read / write operations. * - async_connect: performs the connection asynchronously. * - tcp_nodelay: enables or disables Nagle's algorithm for coalescing. * - persistent: the connection is left intact after a GC collection. - * - * @author Daniele Alessandri + * - ssl: context options array (see http://php.net/manual/en/context.ssl.php) */ class StreamConnection extends AbstractConnection { @@ -47,6 +49,26 @@ class StreamConnection extends AbstractConnection $this->disconnect(); } + /** + * {@inheritdoc} + */ + protected function assertParameters(ParametersInterface $parameters) + { + switch ($parameters->scheme) { + case 'tcp': + case 'redis': + case 'unix': + case 'tls': + case 'rediss': + break; + + default: + throw new InvalidArgumentException("Invalid scheme: '$parameters->scheme'."); + } + + return $parameters; + } + /** * {@inheritdoc} */ @@ -60,11 +82,44 @@ class StreamConnection extends AbstractConnection case 'unix': return $this->unixStreamInitializer($this->parameters); + case 'tls': + case 'rediss': + return $this->tlsStreamInitializer($this->parameters); + default: - throw new \InvalidArgumentException("Invalid scheme: '{$this->parameters->scheme}'."); + throw new InvalidArgumentException("Invalid scheme: '{$this->parameters->scheme}'."); } } + /** + * Creates a connected stream socket resource. + * + * @param ParametersInterface $parameters Connection parameters. + * @param string $address Address for stream_socket_client(). + * @param int $flags Flags for stream_socket_client(). + * + * @return resource + */ + protected function createStreamSocket(ParametersInterface $parameters, $address, $flags) + { + $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0); + $context = stream_context_create(['socket' => ['tcp_nodelay' => (bool) $parameters->tcp_nodelay]]); + + if (!$resource = @stream_socket_client($address, $errno, $errstr, $timeout, $flags, $context)) { + $this->onConnectionError(trim($errstr), $errno); + } + + if (isset($parameters->read_write_timeout)) { + $rwtimeout = (float) $parameters->read_write_timeout; + $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; + $timeoutSeconds = floor($rwtimeout); + $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000; + stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds); + } + + return $resource; + } + /** * Initializes a TCP stream resource. * @@ -75,42 +130,28 @@ class StreamConnection extends AbstractConnection protected function tcpStreamInitializer(ParametersInterface $parameters) { if (!filter_var($parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - $uri = "tcp://$parameters->host:$parameters->port"; + $address = "tcp://$parameters->host:$parameters->port"; } else { - $uri = "tcp://[$parameters->host]:$parameters->port"; + $address = "tcp://[$parameters->host]:$parameters->port"; } $flags = STREAM_CLIENT_CONNECT; - if (isset($parameters->async_connect) && (bool) $parameters->async_connect) { + if (isset($parameters->async_connect) && $parameters->async_connect) { $flags |= STREAM_CLIENT_ASYNC_CONNECT; } - if (isset($parameters->persistent) && (bool) $parameters->persistent) { - $flags |= STREAM_CLIENT_PERSISTENT; - $uri .= strpos($path = $parameters->path, '/') === 0 ? $path : "/$path"; + if (isset($parameters->persistent)) { + if (false !== $persistent = filter_var($parameters->persistent, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) { + $flags |= STREAM_CLIENT_PERSISTENT; + + if ($persistent === null) { + $address = "{$address}/{$parameters->persistent}"; + } + } } - $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags); - - if (!$resource) { - $this->onConnectionError(trim($errstr), $errno); - } - - if (isset($parameters->read_write_timeout)) { - $rwtimeout = (float) $parameters->read_write_timeout; - $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; - $timeoutSeconds = floor($rwtimeout); - $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000; - stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds); - } - - if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) { - $socket = socket_import_stream($resource); - socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay); - } - - return $resource; + return $this->createStreamSocket($parameters, $address, $flags); } /** @@ -126,25 +167,56 @@ class StreamConnection extends AbstractConnection throw new InvalidArgumentException('Missing UNIX domain socket path.'); } - $uri = "unix://{$parameters->path}"; $flags = STREAM_CLIENT_CONNECT; - if ((bool) $parameters->persistent) { - $flags |= STREAM_CLIENT_PERSISTENT; + if (isset($parameters->persistent)) { + if (false !== $persistent = filter_var($parameters->persistent, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) { + $flags |= STREAM_CLIENT_PERSISTENT; + + if ($persistent === null) { + throw new InvalidArgumentException( + 'Persistent connection IDs are not supported when using UNIX domain sockets.' + ); + } + } } - $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags); + return $this->createStreamSocket($parameters, "unix://{$parameters->path}", $flags); + } - if (!$resource) { - $this->onConnectionError(trim($errstr), $errno); + /** + * Initializes a SSL-encrypted TCP stream resource. + * + * @param ParametersInterface $parameters Initialization parameters for the connection. + * + * @return resource + */ + protected function tlsStreamInitializer(ParametersInterface $parameters) + { + $resource = $this->tcpStreamInitializer($parameters); + $metadata = stream_get_meta_data($resource); + + // Detect if crypto mode is already enabled for this stream (PHP >= 7.0.0). + if (isset($metadata['crypto'])) { + return $resource; } - if (isset($parameters->read_write_timeout)) { - $rwtimeout = (float) $parameters->read_write_timeout; - $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; - $timeoutSeconds = floor($rwtimeout); - $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000; - stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds); + if (isset($parameters->ssl) && is_array($parameters->ssl)) { + $options = $parameters->ssl; + } else { + $options = []; + } + + if (!isset($options['crypto_type'])) { + $options['crypto_type'] = STREAM_CRYPTO_METHOD_TLS_CLIENT; + } + + if (!stream_context_set_option($resource, ['ssl' => $options])) { + $this->onConnectionError('Error while setting SSL context options'); + } + + if (!stream_socket_enable_crypto($resource, true, $options['crypto_type'])) { + $this->onConnectionError('Error while switching to encrypted communication'); } return $resource; @@ -157,7 +229,13 @@ class StreamConnection extends AbstractConnection { if (parent::connect() && $this->initCommands) { foreach ($this->initCommands as $command) { - $this->executeCommand($command); + $response = $this->executeCommand($command); + + if ($response instanceof ErrorResponseInterface && $command->getId() === 'CLIENT') { + // Do nothing on CLIENT SETINFO command failure + } elseif ($response instanceof ErrorResponseInterface) { + $this->onConnectionError("`{$command->getId()}` failed: {$response->getMessage()}", 0); + } } } } @@ -168,7 +246,10 @@ class StreamConnection extends AbstractConnection public function disconnect() { if ($this->isConnected()) { - fclose($this->getResource()); + $resource = $this->getResource(); + if (is_resource($resource)) { + fclose($resource); + } parent::disconnect(); } } @@ -184,7 +265,7 @@ class StreamConnection extends AbstractConnection $socket = $this->getResource(); while (($length = strlen($buffer)) > 0) { - $written = @fwrite($socket, $buffer); + $written = is_resource($socket) ? @fwrite($socket, $buffer) : false; if ($length === $written) { return; @@ -228,7 +309,7 @@ class StreamConnection extends AbstractConnection $bytesLeft = ($size += 2); do { - $chunk = fread($socket, min($bytesLeft, 4096)); + $chunk = is_resource($socket) ? fread($socket, min($bytesLeft, 4096)) : false; if ($chunk === false || $chunk === '') { $this->onConnectionError('Error while reading bytes from the server.'); @@ -247,7 +328,7 @@ class StreamConnection extends AbstractConnection return; } - $multibulk = array(); + $multibulk = []; for ($i = 0; $i < $count; ++$i) { $multibulk[$i] = $this->read(); @@ -256,7 +337,9 @@ class StreamConnection extends AbstractConnection return $multibulk; case ':': - return (int) $payload; + $integer = (int) $payload; + + return $integer == $payload ? $integer : $payload; case '-': return new ErrorResponse($payload); @@ -281,9 +364,8 @@ class StreamConnection extends AbstractConnection $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n"; - for ($i = 0, $reqlen--; $i < $reqlen; ++$i) { - $argument = $arguments[$i]; - $arglen = strlen($argument); + foreach ($arguments as $argument) { + $arglen = strlen(strval($argument)); $buffer .= "\${$arglen}\r\n{$argument}\r\n"; } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Connection/WebdisConnection.php b/plugins/cache-redis/Predis/Connection/WebdisConnection.php similarity index 85% rename from snappymail/v/0.0.0/app/libraries/Predis/Connection/WebdisConnection.php rename to plugins/cache-redis/Predis/Connection/WebdisConnection.php index 9cff9d023..bd533783b 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Connection/WebdisConnection.php +++ b/plugins/cache-redis/Predis/Connection/WebdisConnection.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,6 +12,8 @@ namespace Predis\Connection; +use Closure; +use InvalidArgumentException; use Predis\Command\CommandInterface; use Predis\NotSupportedException; use Predis\Protocol\ProtocolException; @@ -33,15 +36,14 @@ use Predis\Response\Status as StatusResponse; * - scheme: must be 'http'. * - host: hostname or IP address of the server. * - port: TCP port of the server. - * - timeout: timeout to perform the connection. + * - timeout: timeout to perform the connection (default is 5 seconds). * - user: username for authentication. * - pass: password for authentication. * - * @link http://webd.is - * @link http://github.com/nicolasff/webdis - * @link http://github.com/seppo0010/phpiredis - * - * @author Daniele Alessandri + * @see http://webd.is + * @see http://github.com/nicolasff/webdis + * @see http://github.com/seppo0010/phpiredis + * @deprecated 2.1.2 */ class WebdisConnection implements NodeConnectionInterface { @@ -52,14 +54,14 @@ class WebdisConnection implements NodeConnectionInterface /** * @param ParametersInterface $parameters Initialization parameters for the connection. * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function __construct(ParametersInterface $parameters) { $this->assertExtensions(); if ($parameters->scheme !== 'http') { - throw new \InvalidArgumentException("Invalid scheme: '{$parameters->scheme}'."); + throw new InvalidArgumentException("Invalid scheme: '{$parameters->scheme}'."); } $this->parameters = $parameters; @@ -117,19 +119,20 @@ class WebdisConnection implements NodeConnectionInterface private function createCurl() { $parameters = $this->getParameters(); + $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0) * 1000; - if (filter_var($host = $parameters->host, FILTER_VALIDATE_IP)) { + if (filter_var($host = $parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $host = "[$host]"; } - $options = array( + $options = [ CURLOPT_FAILONERROR => true, - CURLOPT_CONNECTTIMEOUT_MS => $parameters->timeout * 1000, + CURLOPT_CONNECTTIMEOUT_MS => $timeout, CURLOPT_URL => "$parameters->scheme://$host:$parameters->port", CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_POST => true, - CURLOPT_WRITEFUNCTION => array($this, 'feedReader'), - ); + CURLOPT_WRITEFUNCTION => [$this, 'feedReader'], + ]; if (isset($parameters->user, $parameters->pass)) { $options[CURLOPT_USERPWD] = "{$parameters->user}:{$parameters->pass}"; @@ -158,25 +161,37 @@ class WebdisConnection implements NodeConnectionInterface /** * Returns the handler used by the protocol reader for inline responses. * - * @return \Closure + * @return Closure */ protected function getStatusHandler() { - return function ($payload) { - return StatusResponse::get($payload); - }; + static $statusHandler; + + if (!$statusHandler) { + $statusHandler = function ($payload) { + return StatusResponse::get($payload); + }; + } + + return $statusHandler; } /** * Returns the handler used by the protocol reader for error responses. * - * @return \Closure + * @return Closure */ protected function getErrorHandler() { - return function ($payload) { - return new ErrorResponse($payload); - }; + static $errorHandler; + + if (!$errorHandler) { + $errorHandler = function ($errorMessage) { + return new ErrorResponse($errorMessage); + }; + } + + return $errorHandler; } /** @@ -223,9 +238,8 @@ class WebdisConnection implements NodeConnectionInterface * * @param CommandInterface $command Command instance. * - * @throws NotSupportedException - * * @return string + * @throws NotSupportedException */ protected function getCommandId(CommandInterface $command) { @@ -239,7 +253,6 @@ class WebdisConnection implements NodeConnectionInterface case 'DISCARD': case 'MONITOR': throw new NotSupportedException("Command '$commandID' is not allowed by Webdis."); - default: return $commandID; } @@ -279,10 +292,10 @@ class WebdisConnection implements NodeConnectionInterface curl_setopt($resource, CURLOPT_POSTFIELDS, $serializedCommand); if (curl_exec($resource) === false) { - $error = curl_error($resource); + $error = trim(curl_error($resource)); $errno = curl_errno($resource); - throw new ConnectionException($this, trim($error), $errno); + throw new ConnectionException($this, "$error{$this->getParameters()}]", $errno); } if (phpiredis_reader_get_state($this->reader) !== PHPIREDIS_READER_STATE_COMPLETE) { @@ -337,7 +350,7 @@ class WebdisConnection implements NodeConnectionInterface */ public function __sleep() { - return array('parameters'); + return ['parameters']; } /** diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Monitor/Consumer.php b/plugins/cache-redis/Predis/Monitor/Consumer.php similarity index 79% rename from snappymail/v/0.0.0/app/libraries/Predis/Monitor/Consumer.php rename to plugins/cache-redis/Predis/Monitor/Consumer.php index d10bad1a0..9076bf12f 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Monitor/Consumer.php +++ b/plugins/cache-redis/Predis/Monitor/Consumer.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,16 +12,16 @@ namespace Predis\Monitor; +use Iterator; use Predis\ClientInterface; -use Predis\Connection\AggregateConnectionInterface; +use Predis\Connection\Cluster\ClusterInterface; use Predis\NotSupportedException; +use ReturnTypeWillChange; /** * Redis MONITOR consumer. - * - * @author Daniele Alessandri */ -class Consumer implements \Iterator +class Consumer implements Iterator { private $client; private $valid; @@ -56,14 +57,14 @@ class Consumer implements \Iterator */ private function assertClient(ClientInterface $client) { - if ($client->getConnection() instanceof AggregateConnectionInterface) { + if ($client->getConnection() instanceof ClusterInterface) { throw new NotSupportedException( - 'Cannot initialize a monitor consumer over aggregate connections.' + 'Cannot initialize a monitor consumer over cluster connections.' ); } - if ($client->getProfile()->supportsCommand('MONITOR') === false) { - throw new NotSupportedException("The current profile does not support 'MONITOR'."); + if (!$client->getCommandFactory()->supports('MONITOR')) { + throw new NotSupportedException("'MONITOR' is not supported by the current command factory."); } } @@ -89,8 +90,9 @@ class Consumer implements \Iterator } /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function rewind() { // NOOP @@ -99,24 +101,27 @@ class Consumer implements \Iterator /** * Returns the last message payload retrieved from the server. * - * @return Object + * @return object */ + #[ReturnTypeWillChange] public function current() { return $this->getValue(); } /** - * {@inheritdoc} + * @return int|null */ + #[ReturnTypeWillChange] public function key() { return $this->position; } /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function next() { ++$this->position; @@ -127,6 +132,7 @@ class Consumer implements \Iterator * * @return bool */ + #[ReturnTypeWillChange] public function valid() { return $this->valid; @@ -136,7 +142,7 @@ class Consumer implements \Iterator * Waits for a new message from the server generated by MONITOR and returns * it when available. * - * @return Object + * @return object */ private function getValue() { @@ -160,14 +166,14 @@ class Consumer implements \Iterator }; $event = preg_replace_callback('/ \(db (\d+)\) | \[(\d+) (.*?)\] /', $callback, $event, 1); - @list($timestamp, $command, $arguments) = explode(' ', $event, 3); + @[$timestamp, $command, $arguments] = explode(' ', $event, 3); - return (object) array( + return (object) [ 'timestamp' => (float) $timestamp, 'database' => $database, 'client' => $client, 'command' => substr($command, 1, -1), 'arguments' => $arguments, - ); + ]; } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/NotSupportedException.php b/plugins/cache-redis/Predis/NotSupportedException.php similarity index 78% rename from snappymail/v/0.0.0/app/libraries/Predis/NotSupportedException.php rename to plugins/cache-redis/Predis/NotSupportedException.php index be82aba72..037696b68 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/NotSupportedException.php +++ b/plugins/cache-redis/Predis/NotSupportedException.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,8 +15,6 @@ namespace Predis; /** * Exception class thrown when trying to use features not supported by certain * classes or abstractions of Predis. - * - * @author Daniele Alessandri */ class NotSupportedException extends PredisException { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Pipeline/Atomic.php b/plugins/cache-redis/Predis/Pipeline/Atomic.php similarity index 80% rename from snappymail/v/0.0.0/app/libraries/Predis/Pipeline/Atomic.php rename to plugins/cache-redis/Predis/Pipeline/Atomic.php index 1c9c92aa2..09e19ead6 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Pipeline/Atomic.php +++ b/plugins/cache-redis/Predis/Pipeline/Atomic.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -18,11 +19,10 @@ use Predis\Connection\NodeConnectionInterface; use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\ResponseInterface; use Predis\Response\ServerException; +use SplQueue; /** * Command pipeline wrapped into a MULTI / EXEC transaction. - * - * @author Daniele Alessandri */ class Atomic extends Pipeline { @@ -31,9 +31,9 @@ class Atomic extends Pipeline */ public function __construct(ClientInterface $client) { - if (!$client->getProfile()->supportsCommands(array('multi', 'exec', 'discard'))) { + if (!$client->getCommandFactory()->supports('multi', 'exec', 'discard')) { throw new ClientException( - "The current profile does not support 'MULTI', 'EXEC' and 'DISCARD'." + "'MULTI', 'EXEC' and 'DISCARD' are not supported by the current command factory." ); } @@ -59,10 +59,10 @@ class Atomic extends Pipeline /** * {@inheritdoc} */ - protected function executePipeline(ConnectionInterface $connection, \SplQueue $commands) + protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { - $profile = $this->getClient()->getProfile(); - $connection->executeCommand($profile->createCommand('multi')); + $commandFactory = $this->getClient()->getCommandFactory(); + $connection->executeCommand($commandFactory->create('multi')); foreach ($commands as $command) { $connection->writeRequest($command); @@ -72,15 +72,14 @@ class Atomic extends Pipeline $response = $connection->readResponse($command); if ($response instanceof ErrorResponseInterface) { - $connection->executeCommand($profile->createCommand('discard')); + $connection->executeCommand($commandFactory->create('discard')); throw new ServerException($response->getMessage()); } } - $executed = $connection->executeCommand($profile->createCommand('exec')); + $executed = $connection->executeCommand($commandFactory->create('exec')); if (!isset($executed)) { - // TODO: should be throwing a more appropriate exception. throw new ClientException( 'The underlying transaction has been aborted by the server.' ); @@ -95,7 +94,7 @@ class Atomic extends Pipeline ); } - $responses = array(); + $responses = []; $sizeOfPipe = count($commands); $exceptions = $this->throwServerExceptions(); diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Pipeline/ConnectionErrorProof.php b/plugins/cache-redis/Predis/Pipeline/ConnectionErrorProof.php similarity index 86% rename from snappymail/v/0.0.0/app/libraries/Predis/Pipeline/ConnectionErrorProof.php rename to plugins/cache-redis/Predis/Pipeline/ConnectionErrorProof.php index d3bc732e4..8f995cf07 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Pipeline/ConnectionErrorProof.php +++ b/plugins/cache-redis/Predis/Pipeline/ConnectionErrorProof.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,18 +13,15 @@ namespace Predis\Pipeline; use Predis\CommunicationException; -use Predis\Connection\Aggregate\ClusterInterface; +use Predis\Connection\Cluster\ClusterInterface; use Predis\Connection\ConnectionInterface; use Predis\Connection\NodeConnectionInterface; use Predis\NotSupportedException; +use SplQueue; /** * Command pipeline that does not throw exceptions on connection errors, but * returns the exception instances as the rest of the response elements. - * - * @todo Awful naming! - * - * @author Daniele Alessandri */ class ConnectionErrorProof extends Pipeline { @@ -38,7 +36,7 @@ class ConnectionErrorProof extends Pipeline /** * {@inheritdoc} */ - protected function executePipeline(ConnectionInterface $connection, \SplQueue $commands) + protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { if ($connection instanceof NodeConnectionInterface) { return $this->executeSingleNode($connection, $commands); @@ -54,9 +52,9 @@ class ConnectionErrorProof extends Pipeline /** * {@inheritdoc} */ - protected function executeSingleNode(NodeConnectionInterface $connection, \SplQueue $commands) + protected function executeSingleNode(NodeConnectionInterface $connection, SplQueue $commands) { - $responses = array(); + $responses = []; $sizeOfPipe = count($commands); foreach ($commands as $command) { @@ -86,14 +84,14 @@ class ConnectionErrorProof extends Pipeline /** * {@inheritdoc} */ - protected function executeCluster(ClusterInterface $connection, \SplQueue $commands) + protected function executeCluster(ClusterInterface $connection, SplQueue $commands) { - $responses = array(); + $responses = []; $sizeOfPipe = count($commands); - $exceptions = array(); + $exceptions = []; foreach ($commands as $command) { - $cmdConnection = $connection->getConnection($command); + $cmdConnection = $connection->getConnectionByCommand($command); if (isset($exceptions[spl_object_hash($cmdConnection)])) { continue; @@ -109,7 +107,7 @@ class ConnectionErrorProof extends Pipeline for ($i = 0; $i < $sizeOfPipe; ++$i) { $command = $commands->dequeue(); - $cmdConnection = $connection->getConnection($command); + $cmdConnection = $connection->getConnectionByCommand($command); $connectionHash = spl_object_hash($cmdConnection); if (isset($exceptions[$connectionHash])) { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Pipeline/FireAndForget.php b/plugins/cache-redis/Predis/Pipeline/FireAndForget.php similarity index 80% rename from snappymail/v/0.0.0/app/libraries/Predis/Pipeline/FireAndForget.php rename to plugins/cache-redis/Predis/Pipeline/FireAndForget.php index 95a062b64..75ee88eb2 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Pipeline/FireAndForget.php +++ b/plugins/cache-redis/Predis/Pipeline/FireAndForget.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,18 +13,17 @@ namespace Predis\Pipeline; use Predis\Connection\ConnectionInterface; +use SplQueue; /** * Command pipeline that writes commands to the servers but discards responses. - * - * @author Daniele Alessandri */ class FireAndForget extends Pipeline { /** * {@inheritdoc} */ - protected function executePipeline(ConnectionInterface $connection, \SplQueue $commands) + protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { while (!$commands->isEmpty()) { $connection->writeRequest($commands->dequeue()); @@ -31,6 +31,6 @@ class FireAndForget extends Pipeline $connection->disconnect(); - return array(); + return []; } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Pipeline/Pipeline.php b/plugins/cache-redis/Predis/Pipeline/Pipeline.php similarity index 89% rename from snappymail/v/0.0.0/app/libraries/Predis/Pipeline/Pipeline.php rename to plugins/cache-redis/Predis/Pipeline/Pipeline.php index cf9c59e4f..1f67d0b9f 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Pipeline/Pipeline.php +++ b/plugins/cache-redis/Predis/Pipeline/Pipeline.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,30 +12,31 @@ namespace Predis\Pipeline; +use Exception; +use InvalidArgumentException; use Predis\ClientContextInterface; use Predis\ClientException; use Predis\ClientInterface; use Predis\Command\CommandInterface; -use Predis\Connection\Aggregate\ReplicationInterface; use Predis\Connection\ConnectionInterface; +use Predis\Connection\Replication\ReplicationInterface; use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\ResponseInterface; use Predis\Response\ServerException; +use SplQueue; /** * Implementation of a command pipeline in which write and read operations of * Redis commands are pipelined to alleviate the effects of network round-trips. * * {@inheritdoc} - * - * @author Daniele Alessandri */ class Pipeline implements ClientContextInterface { - private $client; + protected $client; private $pipeline; - private $responses = array(); + private $responses = []; private $running = false; /** @@ -43,7 +45,7 @@ class Pipeline implements ClientContextInterface public function __construct(ClientInterface $client) { $this->client = $client; - $this->pipeline = new \SplQueue(); + $this->pipeline = new SplQueue(); } /** @@ -112,7 +114,7 @@ class Pipeline implements ClientContextInterface $connection = $this->getClient()->getConnection(); if ($connection instanceof ReplicationInterface) { - $connection->switchTo('master'); + $connection->switchToMaster(); } return $connection; @@ -123,17 +125,17 @@ class Pipeline implements ClientContextInterface * from the current connection. * * @param ConnectionInterface $connection Current connection instance. - * @param \SplQueue $commands Queued commands. + * @param SplQueue $commands Queued commands. * * @return array */ - protected function executePipeline(ConnectionInterface $connection, \SplQueue $commands) + protected function executePipeline(ConnectionInterface $connection, SplQueue $commands) { foreach ($commands as $command) { $connection->writeRequest($command); } - $responses = array(); + $responses = []; $exceptions = $this->throwServerExceptions(); while (!$commands->isEmpty()) { @@ -165,7 +167,7 @@ class Pipeline implements ClientContextInterface $responses = $this->executePipeline($this->getConnection(), $this->pipeline); $this->responses = array_merge($this->responses, $responses); } else { - $this->pipeline = new \SplQueue(); + $this->pipeline = new SplQueue(); } return $this; @@ -192,15 +194,14 @@ class Pipeline implements ClientContextInterface * * @param mixed $callable Optional callback for execution. * - * @throws \Exception - * @throws \InvalidArgumentException - * * @return array + * @throws Exception + * @throws InvalidArgumentException */ public function execute($callable = null) { if ($callable && !is_callable($callable)) { - throw new \InvalidArgumentException('The argument must be a callable object.'); + throw new InvalidArgumentException('The argument must be a callable object.'); } $exception = null; @@ -212,7 +213,7 @@ class Pipeline implements ClientContextInterface } $this->flushPipeline(); - } catch (\Exception $exception) { + } catch (Exception $exception) { // NOOP } diff --git a/plugins/cache-redis/Predis/Pipeline/RelayAtomic.php b/plugins/cache-redis/Predis/Pipeline/RelayAtomic.php new file mode 100644 index 000000000..c36e10868 --- /dev/null +++ b/plugins/cache-redis/Predis/Pipeline/RelayAtomic.php @@ -0,0 +1,69 @@ +getClient(); + + $throw = $this->client->getOptions()->exceptions; + + try { + $transaction = $client->multi(); + + foreach ($commands as $command) { + $name = $command->getId(); + + in_array($name, $connection->atypicalCommands) + ? $transaction->{$name}(...$command->getArguments()) + : $transaction->rawCommand($name, ...$command->getArguments()); + } + + $responses = $transaction->exec(); + + if (!is_array($responses)) { + return $responses; + } + + foreach ($responses as $key => $response) { + if ($response instanceof RelayException) { + if ($throw) { + throw $response; + } + + $responses[$key] = new Error($response->getMessage()); + } + } + + return $responses; + } catch (RelayException $ex) { + if ($client->getMode() !== $client::ATOMIC) { + $client->discard(); + } + + throw new ServerException($ex->getMessage(), $ex->getCode(), $ex); + } + } +} diff --git a/plugins/cache-redis/Predis/Pipeline/RelayPipeline.php b/plugins/cache-redis/Predis/Pipeline/RelayPipeline.php new file mode 100644 index 000000000..5f36a0aa4 --- /dev/null +++ b/plugins/cache-redis/Predis/Pipeline/RelayPipeline.php @@ -0,0 +1,75 @@ +getClient(); + + $throw = $this->client->getOptions()->exceptions; + + try { + $pipeline = $client->pipeline(); + + foreach ($commands as $command) { + $name = $command->getId(); + + in_array($name, $connection->atypicalCommands) + ? $pipeline->{$name}(...$command->getArguments()) + : $pipeline->rawCommand($name, ...$command->getArguments()); + } + + $responses = $pipeline->exec(); + + if (!is_array($responses)) { + return $responses; + } + + foreach ($responses as $key => $response) { + if ($response instanceof RelayException) { + if ($throw) { + throw $response; + } + + $responses[$key] = new Error($response->getMessage()); + } + } + + return $responses; + } catch (RelayException $ex) { + if ($client->getMode() !== $client::ATOMIC) { + $client->discard(); + } + + throw new ServerException($ex->getMessage(), $ex->getCode(), $ex); + } + } +} diff --git a/snappymail/v/0.0.0/app/libraries/Predis/PredisException.php b/plugins/cache-redis/Predis/PredisException.php similarity index 63% rename from snappymail/v/0.0.0/app/libraries/Predis/PredisException.php rename to plugins/cache-redis/Predis/PredisException.php index 122bde16d..8e124225d 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/PredisException.php +++ b/plugins/cache-redis/Predis/PredisException.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,11 +12,11 @@ namespace Predis; +use Exception; + /** * Base exception class for Predis-related errors. - * - * @author Daniele Alessandri */ -abstract class PredisException extends \Exception +abstract class PredisException extends Exception { } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/ProtocolException.php b/plugins/cache-redis/Predis/Protocol/ProtocolException.php similarity index 64% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/ProtocolException.php rename to plugins/cache-redis/Predis/Protocol/ProtocolException.php index 6fe5d6d3a..b6ab078a0 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/ProtocolException.php +++ b/plugins/cache-redis/Predis/Protocol/ProtocolException.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,10 +15,8 @@ namespace Predis\Protocol; use Predis\CommunicationException; /** - * Exception used to indentify errors encountered while parsing the Redis wire + * Exception used to identify errors encountered while parsing the Redis wire * protocol. - * - * @author Daniele Alessandri */ class ProtocolException extends CommunicationException { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/ProtocolProcessorInterface.php b/plugins/cache-redis/Predis/Protocol/ProtocolProcessorInterface.php similarity index 91% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/ProtocolProcessorInterface.php rename to plugins/cache-redis/Predis/Protocol/ProtocolProcessorInterface.php index b34ea1814..c36a9bb32 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/ProtocolProcessorInterface.php +++ b/plugins/cache-redis/Predis/Protocol/ProtocolProcessorInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -17,8 +18,6 @@ use Predis\Connection\CompositeConnectionInterface; /** * Defines a pluggable protocol processor capable of serializing commands and * deserializing responses into PHP objects directly from a connection. - * - * @author Daniele Alessandri */ interface ProtocolProcessorInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/RequestSerializerInterface.php b/plugins/cache-redis/Predis/Protocol/RequestSerializerInterface.php similarity index 84% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/RequestSerializerInterface.php rename to plugins/cache-redis/Predis/Protocol/RequestSerializerInterface.php index eef72a640..ba2a14f43 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/RequestSerializerInterface.php +++ b/plugins/cache-redis/Predis/Protocol/RequestSerializerInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,8 +16,6 @@ use Predis\Command\CommandInterface; /** * Defines a pluggable serializer for Redis commands. - * - * @author Daniele Alessandri */ interface RequestSerializerInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/ResponseReaderInterface.php b/plugins/cache-redis/Predis/Protocol/ResponseReaderInterface.php similarity index 86% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/ResponseReaderInterface.php rename to plugins/cache-redis/Predis/Protocol/ResponseReaderInterface.php index 86a7bdcce..ce9c09395 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/ResponseReaderInterface.php +++ b/plugins/cache-redis/Predis/Protocol/ResponseReaderInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,8 +17,6 @@ use Predis\Connection\CompositeConnectionInterface; /** * Defines a pluggable reader capable of parsing responses returned by Redis and * deserializing them to PHP objects. - * - * @author Daniele Alessandri */ interface ResponseReaderInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/CompositeProtocolProcessor.php b/plugins/cache-redis/Predis/Protocol/Text/CompositeProtocolProcessor.php similarity index 94% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/CompositeProtocolProcessor.php rename to plugins/cache-redis/Predis/Protocol/Text/CompositeProtocolProcessor.php index ea85ed303..3f7df02e5 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/CompositeProtocolProcessor.php +++ b/plugins/cache-redis/Predis/Protocol/Text/CompositeProtocolProcessor.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -21,9 +22,7 @@ use Predis\Protocol\ResponseReaderInterface; * Composite protocol processor for the standard Redis wire protocol using * pluggable handlers to serialize requests and deserialize responses. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri + * @see http://redis.io/topics/protocol */ class CompositeProtocolProcessor implements ProtocolProcessorInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/BulkResponse.php b/plugins/cache-redis/Predis/Protocol/Text/Handler/BulkResponse.php similarity index 84% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/BulkResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/BulkResponse.php index 5b0bf3c2d..961c01188 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/BulkResponse.php +++ b/plugins/cache-redis/Predis/Protocol/Text/Handler/BulkResponse.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -19,9 +20,7 @@ use Predis\Protocol\ProtocolException; * Handler for the bulk response type in the standard Redis wire protocol. * It translates the payload to a string or a NULL. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri + * @see http://redis.io/topics/protocol */ class BulkResponse implements ResponseHandlerInterface { @@ -34,7 +33,7 @@ class BulkResponse implements ResponseHandlerInterface if ("$length" !== $payload) { CommunicationException::handle(new ProtocolException( - $connection, "Cannot parse '$payload' as a valid length for a bulk response." + $connection, "Cannot parse '$payload' as a valid length for a bulk response [{$connection->getParameters()}]" )); } @@ -47,7 +46,7 @@ class BulkResponse implements ResponseHandlerInterface } CommunicationException::handle(new ProtocolException( - $connection, "Value '$payload' is not a valid length for a bulk response." + $connection, "Value '$payload' is not a valid length for a bulk response [{$connection->getParameters()}]" )); return; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/ErrorResponse.php b/plugins/cache-redis/Predis/Protocol/Text/Handler/ErrorResponse.php similarity index 82% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/ErrorResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/ErrorResponse.php index 3e18b7b9e..aa400a47b 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/ErrorResponse.php +++ b/plugins/cache-redis/Predis/Protocol/Text/Handler/ErrorResponse.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -18,9 +19,7 @@ use Predis\Response\Error; * Handler for the error response type in the standard Redis wire protocol. * It translates the payload to a complex response object for Predis. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri + * @see http://redis.io/topics/protocol */ class ErrorResponse implements ResponseHandlerInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/IntegerResponse.php b/plugins/cache-redis/Predis/Protocol/Text/Handler/IntegerResponse.php similarity index 78% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/IntegerResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/IntegerResponse.php index 4639d7792..cb58a3d42 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/IntegerResponse.php +++ b/plugins/cache-redis/Predis/Protocol/Text/Handler/IntegerResponse.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -19,9 +20,7 @@ use Predis\Protocol\ProtocolException; * Handler for the integer response type in the standard Redis wire protocol. * It translates the payload an integer or NULL. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri + * @see http://redis.io/topics/protocol */ class IntegerResponse implements ResponseHandlerInterface { @@ -31,12 +30,14 @@ class IntegerResponse implements ResponseHandlerInterface public function handle(CompositeConnectionInterface $connection, $payload) { if (is_numeric($payload)) { - return (int) $payload; + $integer = (int) $payload; + + return $integer == $payload ? $integer : $payload; } if ($payload !== 'nil') { CommunicationException::handle(new ProtocolException( - $connection, "Cannot parse '$payload' as a valid numeric response." + $connection, "Cannot parse '$payload' as a valid numeric response [{$connection->getParameters()}]" )); } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/MultiBulkResponse.php b/plugins/cache-redis/Predis/Protocol/Text/Handler/MultiBulkResponse.php similarity index 86% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/MultiBulkResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/MultiBulkResponse.php index 820b9b4a6..d9c51425d 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/MultiBulkResponse.php +++ b/plugins/cache-redis/Predis/Protocol/Text/Handler/MultiBulkResponse.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -19,9 +20,7 @@ use Predis\Protocol\ProtocolException; * Handler for the multibulk response type in the standard Redis wire protocol. * It returns multibulk responses as PHP arrays. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri + * @see http://redis.io/topics/protocol */ class MultiBulkResponse implements ResponseHandlerInterface { @@ -34,7 +33,7 @@ class MultiBulkResponse implements ResponseHandlerInterface if ("$length" !== $payload) { CommunicationException::handle(new ProtocolException( - $connection, "Cannot parse '$payload' as a valid length of a multi-bulk response." + $connection, "Cannot parse '$payload' as a valid length of a multi-bulk response [{$connection->getParameters()}]" )); } @@ -42,10 +41,10 @@ class MultiBulkResponse implements ResponseHandlerInterface return; } - $list = array(); + $list = []; if ($length > 0) { - $handlersCache = array(); + $handlersCache = []; $reader = $connection->getProtocol()->getResponseReader(); for ($i = 0; $i < $length; ++$i) { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/ResponseHandlerInterface.php b/plugins/cache-redis/Predis/Protocol/Text/Handler/ResponseHandlerInterface.php similarity index 88% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/ResponseHandlerInterface.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/ResponseHandlerInterface.php index ca08a9c53..b1c90665a 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/ResponseHandlerInterface.php +++ b/plugins/cache-redis/Predis/Protocol/Text/Handler/ResponseHandlerInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,8 +16,6 @@ use Predis\Connection\CompositeConnectionInterface; /** * Defines a pluggable handler used to parse a particular type of response. - * - * @author Daniele Alessandri */ interface ResponseHandlerInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/StatusResponse.php b/plugins/cache-redis/Predis/Protocol/Text/Handler/StatusResponse.php similarity index 83% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/StatusResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/StatusResponse.php index 7bde5558f..efc13656a 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/StatusResponse.php +++ b/plugins/cache-redis/Predis/Protocol/Text/Handler/StatusResponse.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -19,9 +20,7 @@ use Predis\Response\Status; * translates certain classes of status response to PHP objects or just returns * the payload as a string. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri + * @see http://redis.io/topics/protocol */ class StatusResponse implements ResponseHandlerInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/StreamableMultiBulkResponse.php b/plugins/cache-redis/Predis/Protocol/Text/Handler/StreamableMultiBulkResponse.php similarity index 87% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/StreamableMultiBulkResponse.php rename to plugins/cache-redis/Predis/Protocol/Text/Handler/StreamableMultiBulkResponse.php index 7cdb736af..7738e9dbe 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/Handler/StreamableMultiBulkResponse.php +++ b/plugins/cache-redis/Predis/Protocol/Text/Handler/StreamableMultiBulkResponse.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -23,9 +24,7 @@ use Predis\Response\Iterator\MultiBulk as MultiBulkIterator; * Streamable multibulk responses are not globally supported by the abstractions * built-in into Predis, such as transactions or pipelines. Use them with care! * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri + * @see http://redis.io/topics/protocol */ class StreamableMultiBulkResponse implements ResponseHandlerInterface { @@ -38,7 +37,7 @@ class StreamableMultiBulkResponse implements ResponseHandlerInterface if ("$length" != $payload) { CommunicationException::handle(new ProtocolException( - $connection, "Cannot parse '$payload' as a valid length for a multi-bulk response." + $connection, "Cannot parse '$payload' as a valid length for a multi-bulk response [{$connection->getParameters()}]" )); } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/ProtocolProcessor.php b/plugins/cache-redis/Predis/Protocol/Text/ProtocolProcessor.php similarity index 91% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/ProtocolProcessor.php rename to plugins/cache-redis/Predis/Protocol/Text/ProtocolProcessor.php index f04c3ed5b..02e0c5d58 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/ProtocolProcessor.php +++ b/plugins/cache-redis/Predis/Protocol/Text/ProtocolProcessor.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -23,18 +24,13 @@ use Predis\Response\Status as StatusResponse; /** * Protocol processor for the standard Redis wire protocol. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri + * @see http://redis.io/topics/protocol */ class ProtocolProcessor implements ProtocolProcessorInterface { protected $mbiterable; protected $serializer; - /** - * - */ public function __construct() { $this->mbiterable = false; @@ -81,7 +77,7 @@ class ProtocolProcessor implements ProtocolProcessorInterface return new MultiBulkIterator($connection, $count); } - $multibulk = array(); + $multibulk = []; for ($i = 0; $i < $count; ++$i) { $multibulk[$i] = $this->read($connection); @@ -90,14 +86,16 @@ class ProtocolProcessor implements ProtocolProcessorInterface return $multibulk; case ':': - return (int) $payload; + $integer = (int) $payload; + + return $integer == $payload ? $integer : $payload; case '-': return new ErrorResponse($payload); default: CommunicationException::handle(new ProtocolException( - $connection, "Unknown response prefix: '$prefix'." + $connection, "Unknown response prefix: '$prefix' [{$connection->getParameters()}]" )); return; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/RequestSerializer.php b/plugins/cache-redis/Predis/Protocol/Text/RequestSerializer.php similarity index 79% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/RequestSerializer.php rename to plugins/cache-redis/Predis/Protocol/Text/RequestSerializer.php index c8cbbfbcd..853bae03a 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/RequestSerializer.php +++ b/plugins/cache-redis/Predis/Protocol/Text/RequestSerializer.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -17,9 +18,7 @@ use Predis\Protocol\RequestSerializerInterface; /** * Request serializer for the standard Redis wire protocol. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri + * @see http://redis.io/topics/protocol */ class RequestSerializer implements RequestSerializerInterface { @@ -36,8 +35,7 @@ class RequestSerializer implements RequestSerializerInterface $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n"; - for ($i = 0, $reqlen--; $i < $reqlen; ++$i) { - $argument = $arguments[$i]; + foreach ($arguments as $argument) { $arglen = strlen($argument); $buffer .= "\${$arglen}\r\n{$argument}\r\n"; } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/ResponseReader.php b/plugins/cache-redis/Predis/Protocol/Text/ResponseReader.php similarity index 86% rename from snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/ResponseReader.php rename to plugins/cache-redis/Predis/Protocol/Text/ResponseReader.php index d96218dfa..f49c96d26 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Protocol/Text/ResponseReader.php +++ b/plugins/cache-redis/Predis/Protocol/Text/ResponseReader.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -19,17 +20,12 @@ use Predis\Protocol\ResponseReaderInterface; /** * Response reader for the standard Redis wire protocol. * - * @link http://redis.io/topics/protocol - * - * @author Daniele Alessandri + * @see http://redis.io/topics/protocol */ class ResponseReader implements ResponseReaderInterface { protected $handlers; - /** - * - */ public function __construct() { $this->handlers = $this->getDefaultHandlers(); @@ -42,13 +38,13 @@ class ResponseReader implements ResponseReaderInterface */ protected function getDefaultHandlers() { - return array( + return [ '+' => new Handler\StatusResponse(), '-' => new Handler\ErrorResponse(), ':' => new Handler\IntegerResponse(), '$' => new Handler\BulkResponse(), '*' => new Handler\MultiBulkResponse(), - ); + ]; } /** @@ -86,18 +82,16 @@ class ResponseReader implements ResponseReaderInterface $header = $connection->readLine(); if ($header === '') { - $this->onProtocolError($connection, 'Unexpected empty reponse header.'); + $this->onProtocolError($connection, 'Unexpected empty response header'); } $prefix = $header[0]; if (!isset($this->handlers[$prefix])) { - $this->onProtocolError($connection, "Unknown response prefix: '$prefix'."); + $this->onProtocolError($connection, "Unknown response prefix: '$prefix'"); } - $payload = $this->handlers[$prefix]->handle($connection, substr($header, 1)); - - return $payload; + return $this->handlers[$prefix]->handle($connection, substr($header, 1)); } /** @@ -110,7 +104,7 @@ class ResponseReader implements ResponseReaderInterface protected function onProtocolError(CompositeConnectionInterface $connection, $message) { CommunicationException::handle( - new ProtocolException($connection, $message) + new ProtocolException($connection, "$message [{$connection->getParameters()}]") ); } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/PubSub/AbstractConsumer.php b/plugins/cache-redis/Predis/PubSub/AbstractConsumer.php similarity index 75% rename from snappymail/v/0.0.0/app/libraries/Predis/PubSub/AbstractConsumer.php rename to plugins/cache-redis/Predis/PubSub/AbstractConsumer.php index d7423f1e3..c9ea2d658 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/PubSub/AbstractConsumer.php +++ b/plugins/cache-redis/Predis/PubSub/AbstractConsumer.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,27 +12,28 @@ namespace Predis\PubSub; +use Iterator; +use ReturnTypeWillChange; + /** * Base implementation of a PUB/SUB consumer abstraction based on PHP iterators. - * - * @author Daniele Alessandri */ -abstract class AbstractConsumer implements \Iterator +abstract class AbstractConsumer implements Iterator { - const SUBSCRIBE = 'subscribe'; - const UNSUBSCRIBE = 'unsubscribe'; - const PSUBSCRIBE = 'psubscribe'; - const PUNSUBSCRIBE = 'punsubscribe'; - const MESSAGE = 'message'; - const PMESSAGE = 'pmessage'; - const PONG = 'pong'; + public const SUBSCRIBE = 'subscribe'; + public const UNSUBSCRIBE = 'unsubscribe'; + public const PSUBSCRIBE = 'psubscribe'; + public const PUNSUBSCRIBE = 'punsubscribe'; + public const MESSAGE = 'message'; + public const PMESSAGE = 'pmessage'; + public const PONG = 'pong'; - const STATUS_VALID = 1; // 0b0001 - const STATUS_SUBSCRIBED = 2; // 0b0010 - const STATUS_PSUBSCRIBED = 4; // 0b0100 + public const STATUS_VALID = 1; // 0b0001 + public const STATUS_SUBSCRIBED = 2; // 0b0010 + public const STATUS_PSUBSCRIBED = 4; // 0b0100 - private $position = null; - private $statusFlags = self::STATUS_VALID; + protected $position; + protected $statusFlags = self::STATUS_VALID; /** * Automatically stops the consumer when the garbage collector kicks in. @@ -56,9 +58,9 @@ abstract class AbstractConsumer implements \Iterator /** * Subscribes to the specified channels. * - * @param mixed $channel,... One or more channel names. + * @param string ...$channel One or more channel names. */ - public function subscribe($channel /*, ... */) + public function subscribe($channel /* , ... */) { $this->writeRequest(self::SUBSCRIBE, func_get_args()); $this->statusFlags |= self::STATUS_SUBSCRIBED; @@ -67,9 +69,9 @@ abstract class AbstractConsumer implements \Iterator /** * Unsubscribes from the specified channels. * - * @param string ... One or more channel names. + * @param string ...$channel One or more channel names. */ - public function unsubscribe(/* ... */) + public function unsubscribe(...$channel) { $this->writeRequest(self::UNSUBSCRIBE, func_get_args()); } @@ -77,9 +79,9 @@ abstract class AbstractConsumer implements \Iterator /** * Subscribes to the specified channels using a pattern. * - * @param mixed $pattern,... One or more channel name patterns. + * @param string ...$pattern One or more channel name patterns. */ - public function psubscribe($pattern /* ... */) + public function psubscribe(...$pattern) { $this->writeRequest(self::PSUBSCRIBE, func_get_args()); $this->statusFlags |= self::STATUS_PSUBSCRIBED; @@ -88,9 +90,9 @@ abstract class AbstractConsumer implements \Iterator /** * Unsubscribes from the specified channels using a pattern. * - * @param string ... One or more channel name patterns. + * @param string ...$pattern One or more channel name patterns. */ - public function punsubscribe(/* ... */) + public function punsubscribe(...$pattern) { $this->writeRequest(self::PUNSUBSCRIBE, func_get_args()); } @@ -103,7 +105,7 @@ abstract class AbstractConsumer implements \Iterator */ public function ping($payload = null) { - $this->writeRequest('PING', array($payload)); + $this->writeRequest('PING', [$payload]); } /** @@ -149,8 +151,9 @@ abstract class AbstractConsumer implements \Iterator abstract protected function writeRequest($method, $arguments); /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function rewind() { // NOOP @@ -162,22 +165,25 @@ abstract class AbstractConsumer implements \Iterator * * @return array */ + #[ReturnTypeWillChange] public function current() { return $this->getValue(); } /** - * {@inheritdoc} + * @return int|null */ + #[ReturnTypeWillChange] public function key() { return $this->position; } /** - * {@inheritdoc} + * @return int|null */ + #[ReturnTypeWillChange] public function next() { if ($this->valid()) { @@ -192,6 +198,7 @@ abstract class AbstractConsumer implements \Iterator * * @return bool */ + #[ReturnTypeWillChange] public function valid() { $isValid = $this->isFlagSet(self::STATUS_VALID); diff --git a/snappymail/v/0.0.0/app/libraries/Predis/PubSub/Consumer.php b/plugins/cache-redis/Predis/PubSub/Consumer.php similarity index 77% rename from snappymail/v/0.0.0/app/libraries/Predis/PubSub/Consumer.php rename to plugins/cache-redis/Predis/PubSub/Consumer.php index 5f2d8a8bc..b52673232 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/PubSub/Consumer.php +++ b/plugins/cache-redis/Predis/PubSub/Consumer.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,18 +15,16 @@ namespace Predis\PubSub; use Predis\ClientException; use Predis\ClientInterface; use Predis\Command\Command; -use Predis\Connection\AggregateConnectionInterface; +use Predis\Connection\Cluster\ClusterInterface; use Predis\NotSupportedException; /** - * PUB/SUB consumer abstraction. - * - * @author Daniele Alessandri + * PUB/SUB consumer. */ class Consumer extends AbstractConsumer { - private $client; - private $options; + protected $client; + protected $options; /** * @param ClientInterface $client Client instance used by the consumer. @@ -35,7 +34,7 @@ class Consumer extends AbstractConsumer { $this->checkCapabilities($client); - $this->options = $options ?: array(); + $this->options = $options ?: []; $this->client = $client; $this->genericSubscribeInit('subscribe'); @@ -60,19 +59,19 @@ class Consumer extends AbstractConsumer * * @throws NotSupportedException */ - private function checkCapabilities(ClientInterface $client) + protected function checkCapabilities(ClientInterface $client) { - if ($client->getConnection() instanceof AggregateConnectionInterface) { + if ($client->getConnection() instanceof ClusterInterface) { throw new NotSupportedException( - 'Cannot initialize a PUB/SUB consumer over aggregate connections.' + 'Cannot initialize a PUB/SUB consumer over cluster connections.' ); } - $commands = array('publish', 'subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe'); + $commands = ['publish', 'subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe']; - if ($client->getProfile()->supportsCommands($commands) === false) { + if (!$client->getCommandFactory()->supports(...$commands)) { throw new NotSupportedException( - 'The current profile does not support PUB/SUB related commands.' + 'PUB/SUB commands are not supported by the current command factory.' ); } } @@ -82,7 +81,7 @@ class Consumer extends AbstractConsumer * * @param string $subscribeAction Type of subscription. */ - private function genericSubscribeInit($subscribeAction) + protected function genericSubscribeInit($subscribeAction) { if (isset($this->options[$subscribeAction])) { $this->$subscribeAction($this->options[$subscribeAction]); @@ -129,25 +128,25 @@ class Consumer extends AbstractConsumer // no break case self::MESSAGE: - return (object) array( + return (object) [ 'kind' => $response[0], 'channel' => $response[1], 'payload' => $response[2], - ); + ]; case self::PMESSAGE: - return (object) array( + return (object) [ 'kind' => $response[0], 'pattern' => $response[1], 'channel' => $response[2], 'payload' => $response[3], - ); + ]; case self::PONG: - return (object) array( + return (object) [ 'kind' => $response[0], 'payload' => $response[1], - ); + ]; default: throw new ClientException( diff --git a/snappymail/v/0.0.0/app/libraries/Predis/PubSub/DispatcherLoop.php b/plugins/cache-redis/Predis/PubSub/DispatcherLoop.php similarity index 85% rename from snappymail/v/0.0.0/app/libraries/Predis/PubSub/DispatcherLoop.php rename to plugins/cache-redis/Predis/PubSub/DispatcherLoop.php index 0d4a08ef7..b6f79cd3d 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/PubSub/DispatcherLoop.php +++ b/plugins/cache-redis/Predis/PubSub/DispatcherLoop.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,11 +12,11 @@ namespace Predis\PubSub; +use InvalidArgumentException; + /** * Method-dispatcher loop built around the client-side abstraction of a Redis * PUB / SUB context. - * - * @author Daniele Alessandri */ class DispatcherLoop { @@ -30,7 +31,7 @@ class DispatcherLoop */ public function __construct(Consumer $pubsub) { - $this->callbacks = array(); + $this->callbacks = []; $this->pubsub = $pubsub; } @@ -39,12 +40,12 @@ class DispatcherLoop * * @param mixed $callable A callback. * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ protected function assertCallback($callable) { if (!is_callable($callable)) { - throw new \InvalidArgumentException('The given argument must be a callable object.'); + throw new InvalidArgumentException('The given argument must be a callable object.'); } } @@ -91,11 +92,11 @@ class DispatcherLoop * Binds a callback to a channel. * * @param string $channel Channel name. - * @param Callable $callback A callback. + * @param callable $callback A callback. */ public function attachCallback($channel, $callback) { - $callbackName = $this->getPrefixKeys().$channel; + $callbackName = $this->getPrefixKeys() . $channel; $this->assertCallback($callback); $this->callbacks[$callbackName] = $callback; @@ -109,7 +110,7 @@ class DispatcherLoop */ public function detachCallback($channel) { - $callbackName = $this->getPrefixKeys().$channel; + $callbackName = $this->getPrefixKeys() . $channel; if (isset($this->callbacks[$callbackName])) { unset($this->callbacks[$callbackName]); @@ -128,7 +129,7 @@ class DispatcherLoop if ($kind !== Consumer::MESSAGE && $kind !== Consumer::PMESSAGE) { if (isset($this->subscriptionCallback)) { $callback = $this->subscriptionCallback; - call_user_func($callback, $message); + call_user_func($callback, $message, $this); } continue; @@ -136,10 +137,10 @@ class DispatcherLoop if (isset($this->callbacks[$message->channel])) { $callback = $this->callbacks[$message->channel]; - call_user_func($callback, $message->payload); + call_user_func($callback, $message->payload, $this); } elseif (isset($this->defaultCallback)) { $callback = $this->defaultCallback; - call_user_func($callback, $message); + call_user_func($callback, $message, $this); } } } diff --git a/plugins/cache-redis/Predis/PubSub/RelayConsumer.php b/plugins/cache-redis/Predis/PubSub/RelayConsumer.php new file mode 100644 index 000000000..2af67b844 --- /dev/null +++ b/plugins/cache-redis/Predis/PubSub/RelayConsumer.php @@ -0,0 +1,114 @@ +statusFlags |= self::STATUS_SUBSCRIBED; + + $command = $this->client->createCommand('subscribe', [ + $channels, + function ($relay, $channel, $message) use ($callback) { + $callback((object) [ + 'kind' => is_null($message) ? self::SUBSCRIBE : self::MESSAGE, + 'channel' => $channel, + 'payload' => $message, + ], $relay); + }, + ]); + + $this->client->getConnection()->executeCommand($command); + + $this->invalidate(); + } + + /** + * Subscribes to the specified channels using a pattern. + * + * @param string ...$pattern One or more channel name patterns. + * @param callable $callback The message callback. + */ + public function psubscribe(...$pattern) // @phpstan-ignore-line + { + $patterns = func_get_args(); + $callback = array_pop($patterns); + + $this->statusFlags |= self::STATUS_PSUBSCRIBED; + + $command = $this->client->createCommand('psubscribe', [ + $patterns, + function ($relay, $pattern, $channel, $message) use ($callback) { + $callback((object) [ + 'kind' => is_null($message) ? self::PSUBSCRIBE : self::PMESSAGE, + 'pattern' => $pattern, + 'channel' => $channel, + 'payload' => $message, + ], $relay); + }, + ]); + + $this->client->getConnection()->executeCommand($command); + + $this->invalidate(); + } + + /** + * {@inheritDoc} + */ + protected function genericSubscribeInit($subscribeAction) + { + if (isset($this->options[$subscribeAction])) { + throw new NotSupportedException('Relay does not support Pub/Sub constructor options.'); + } + } + + /** + * {@inheritDoc} + */ + public function ping($payload = null) + { + throw new NotSupportedException('Relay does not support PING in Pub/Sub.'); + } + + /** + * {@inheritDoc} + */ + public function stop($drop = false) + { + return false; + } + + /** + * {@inheritDoc} + */ + public function __destruct() + { + // NOOP + } +} diff --git a/plugins/cache-redis/Predis/Replication/MissingMasterException.php b/plugins/cache-redis/Predis/Replication/MissingMasterException.php new file mode 100644 index 000000000..d30c259d0 --- /dev/null +++ b/plugins/cache-redis/Predis/Replication/MissingMasterException.php @@ -0,0 +1,22 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,23 +17,19 @@ use Predis\NotSupportedException; /** * Defines a strategy for master/slave replication. - * - * @author Daniele Alessandri */ class ReplicationStrategy { protected $disallowed; protected $readonly; protected $readonlySHA1; + protected $loadBalancing = true; - /** - * - */ public function __construct() { $this->disallowed = $this->getDisallowedOperations(); $this->readonly = $this->getReadOnlyOperations(); - $this->readonlySHA1 = array(); + $this->readonlySHA1 = []; } /** @@ -41,12 +38,15 @@ class ReplicationStrategy * * @param CommandInterface $command Command instance. * - * @throws NotSupportedException - * * @return bool + * @throws NotSupportedException */ public function isReadOperation(CommandInterface $command) { + if (!$this->loadBalancing) { + return false; + } + if (isset($this->disallowed[$id = $command->getId()])) { throw new NotSupportedException( "The command '$id' is not allowed in replication mode." @@ -62,7 +62,8 @@ class ReplicationStrategy } if (($eval = $id === 'EVAL') || $id === 'EVALSHA') { - $sha1 = $eval ? sha1($command->getArgument(0)) : $command->getArgument(0); + $argument = $command->getArgument(0); + $sha1 = $eval ? sha1(strval($argument)) : $argument; if (isset($this->readonlySHA1[$sha1])) { if (true === $readonly = $this->readonlySHA1[$sha1]) { @@ -90,18 +91,54 @@ class ReplicationStrategy } /** - * Checks if a SORT command is a readable operation by parsing the arguments - * array of the specified commad instance. + * Checks if BITFIELD performs a read-only operation by looking for certain + * SET and INCRYBY modifiers in the arguments array of the command. * * @param CommandInterface $command Command instance. * * @return bool */ - protected function isSortReadOnly(CommandInterface $command) + protected function isBitfieldReadOnly(CommandInterface $command) { $arguments = $command->getArguments(); + $argc = count($arguments); - return ($c = count($arguments)) === 1 ? true : $arguments[$c - 2] !== 'STORE'; + if ($argc >= 2) { + for ($i = 1; $i < $argc; ++$i) { + $argument = strtoupper($arguments[$i]); + if ($argument === 'SET' || $argument === 'INCRBY') { + return false; + } + } + } + + return true; + } + + /** + * Checks if a GEORADIUS command is a readable operation by parsing the + * arguments array of the specified command instance. + * + * @param CommandInterface $command Command instance. + * + * @return bool + */ + protected function isGeoradiusReadOnly(CommandInterface $command) + { + $arguments = $command->getArguments(); + $argc = count($arguments); + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; + + if ($argc > $startIndex) { + for ($i = $startIndex; $i < $argc; ++$i) { + $argument = strtoupper($arguments[$i]); + if ($argument === 'STORE' || $argument === 'STOREDIST') { + return false; + } + } + } + + return true; } /** @@ -153,7 +190,7 @@ class ReplicationStrategy */ protected function getDisallowedOperations() { - return array( + return [ 'SHUTDOWN' => true, 'INFO' => true, 'DBSIZE' => true, @@ -165,7 +202,7 @@ class ReplicationStrategy 'BGSAVE' => true, 'BGREWRITEAOF' => true, 'SLOWLOG' => true, - ); + ]; } /** @@ -175,7 +212,7 @@ class ReplicationStrategy */ protected function getReadOnlyOperations() { - return array( + return [ 'EXISTS' => true, 'TYPE' => true, 'KEYS' => true, @@ -231,7 +268,25 @@ class ReplicationStrategy 'BITPOS' => true, 'TIME' => true, 'PFCOUNT' => true, - 'SORT' => array($this, 'isSortReadOnly'), - ); + 'BITFIELD' => [$this, 'isBitfieldReadOnly'], + 'GEOHASH' => true, + 'GEOPOS' => true, + 'GEODIST' => true, + 'GEORADIUS' => [$this, 'isGeoradiusReadOnly'], + 'GEORADIUSBYMEMBER' => [$this, 'isGeoradiusReadOnly'], + ]; + } + + /** + * Disables reads to slaves when using + * a replication topology. + * + * @return self + */ + public function disableLoadBalancing(): self + { + $this->loadBalancing = false; + + return $this; } } diff --git a/plugins/cache-redis/Predis/Replication/RoleException.php b/plugins/cache-redis/Predis/Replication/RoleException.php new file mode 100644 index 000000000..968b7e2cd --- /dev/null +++ b/plugins/cache-redis/Predis/Replication/RoleException.php @@ -0,0 +1,23 @@ + + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,8 +15,6 @@ namespace Predis\Response; /** * Represents an error returned by Redis (-ERR responses) during the execution * of a command on the server. - * - * @author Daniele Alessandri */ class Error implements ErrorInterface { @@ -42,7 +41,7 @@ class Error implements ErrorInterface */ public function getErrorType() { - list($errorType) = explode(' ', $this->getMessage(), 2); + [$errorType] = explode(' ', $this->getMessage(), 2); return $errorType; } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Response/ErrorInterface.php b/plugins/cache-redis/Predis/Response/ErrorInterface.php similarity index 86% rename from snappymail/v/0.0.0/app/libraries/Predis/Response/ErrorInterface.php rename to plugins/cache-redis/Predis/Response/ErrorInterface.php index a4a4a02f7..ac3bb16e8 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Response/ErrorInterface.php +++ b/plugins/cache-redis/Predis/Response/ErrorInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,8 +15,6 @@ namespace Predis\Response; /** * Represents an error returned by Redis (responses identified by "-" in the * Redis protocol) during the execution of an operation on the server. - * - * @author Daniele Alessandri */ interface ErrorInterface extends ResponseInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulk.php b/plugins/cache-redis/Predis/Response/Iterator/MultiBulk.php similarity index 94% rename from snappymail/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulk.php rename to plugins/cache-redis/Predis/Response/Iterator/MultiBulk.php index b1d29241c..09f4c08e5 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulk.php +++ b/plugins/cache-redis/Predis/Response/Iterator/MultiBulk.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,8 +16,6 @@ use Predis\Connection\NodeConnectionInterface; /** * Streamable multibulk response. - * - * @author Daniele Alessandri */ class MultiBulk extends MultiBulkIterator { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulkIterator.php b/plugins/cache-redis/Predis/Response/Iterator/MultiBulkIterator.php similarity index 80% rename from snappymail/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulkIterator.php rename to plugins/cache-redis/Predis/Response/Iterator/MultiBulkIterator.php index 5d328869b..cbc613859 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulkIterator.php +++ b/plugins/cache-redis/Predis/Response/Iterator/MultiBulkIterator.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,7 +12,10 @@ namespace Predis\Response\Iterator; +use Countable; +use Iterator; use Predis\Response\ResponseInterface; +use ReturnTypeWillChange; /** * Iterator that abstracts the access to multibulk responses allowing them to be @@ -22,42 +26,44 @@ use Predis\Response\ResponseInterface; * * Always make sure that the whole iteration is consumed (or dropped) to prevent * protocol desynchronization issues. - * - * @author Daniele Alessandri */ -abstract class MultiBulkIterator implements \Iterator, \Countable, ResponseInterface +abstract class MultiBulkIterator implements Iterator, Countable, ResponseInterface { protected $current; protected $position; protected $size; /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function rewind() { // NOOP } /** - * {@inheritdoc} + * @return mixed */ + #[ReturnTypeWillChange] public function current() { return $this->current; } /** - * {@inheritdoc} + * @return int|null */ + #[ReturnTypeWillChange] public function key() { return $this->position; } /** - * {@inheritdoc} + * @return void */ + #[ReturnTypeWillChange] public function next() { if (++$this->position < $this->size) { @@ -66,8 +72,9 @@ abstract class MultiBulkIterator implements \Iterator, \Countable, ResponseInter } /** - * {@inheritdoc} + * @return bool */ + #[ReturnTypeWillChange] public function valid() { return $this->position < $this->size; @@ -82,6 +89,7 @@ abstract class MultiBulkIterator implements \Iterator, \Countable, ResponseInter * * @return int */ + #[ReturnTypeWillChange] public function count() { return $this->size; diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulkTuple.php b/plugins/cache-redis/Predis/Response/Iterator/MultiBulkTuple.php similarity index 75% rename from snappymail/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulkTuple.php rename to plugins/cache-redis/Predis/Response/Iterator/MultiBulkTuple.php index 2b6f593c4..4761f0ec2 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Response/Iterator/MultiBulkTuple.php +++ b/plugins/cache-redis/Predis/Response/Iterator/MultiBulkTuple.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,16 +12,19 @@ namespace Predis\Response\Iterator; +use InvalidArgumentException; +use OuterIterator; +use ReturnTypeWillChange; +use UnexpectedValueException; + /** * Outer iterator consuming streamable multibulk responses by yielding tuples of * keys and values. * * This wrapper is useful for responses to commands such as `HGETALL` that can - * be iterater as $key => $value pairs. - * - * @author Daniele Alessandri + * be iterator as $key => $value pairs. */ -class MultiBulkTuple extends MultiBulk implements \OuterIterator +class MultiBulkTuple extends MultiBulk implements OuterIterator { private $iterator; @@ -42,25 +46,26 @@ class MultiBulkTuple extends MultiBulk implements \OuterIterator * * @param MultiBulk $iterator Inner multibulk response iterator. * - * @throws \InvalidArgumentException - * @throws \UnexpectedValueException + * @throws InvalidArgumentException + * @throws UnexpectedValueException */ protected function checkPreconditions(MultiBulk $iterator) { if ($iterator->getPosition() !== 0) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'Cannot initialize a tuple iterator using an already initiated iterator.' ); } if (($size = count($iterator)) % 2 !== 0) { - throw new \UnexpectedValueException('Invalid response size for a tuple iterator.'); + throw new UnexpectedValueException('Invalid response size for a tuple iterator.'); } } /** - * {@inheritdoc} + * @return MultiBulk */ + #[ReturnTypeWillChange] public function getInnerIterator() { return $this->iterator; @@ -85,6 +90,6 @@ class MultiBulkTuple extends MultiBulk implements \OuterIterator $v = $this->iterator->current(); $this->iterator->next(); - return array($k, $v); + return [$k, $v]; } } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Response/ResponseInterface.php b/plugins/cache-redis/Predis/Response/ResponseInterface.php similarity index 74% rename from snappymail/v/0.0.0/app/libraries/Predis/Response/ResponseInterface.php rename to plugins/cache-redis/Predis/Response/ResponseInterface.php index 0af135745..d2f682806 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Response/ResponseInterface.php +++ b/plugins/cache-redis/Predis/Response/ResponseInterface.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,8 +14,6 @@ namespace Predis\Response; /** * Represents a complex response object from Redis. - * - * @author Daniele Alessandri */ interface ResponseInterface { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Response/ServerException.php b/plugins/cache-redis/Predis/Response/ServerException.php similarity index 82% rename from snappymail/v/0.0.0/app/libraries/Predis/Response/ServerException.php rename to plugins/cache-redis/Predis/Response/ServerException.php index 407dc5b76..305fe4091 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Response/ServerException.php +++ b/plugins/cache-redis/Predis/Response/ServerException.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,8 +16,6 @@ use Predis\PredisException; /** * Exception class that identifies server-side Redis errors. - * - * @author Daniele Alessandri */ class ServerException extends PredisException implements ErrorInterface { @@ -27,7 +26,7 @@ class ServerException extends PredisException implements ErrorInterface */ public function getErrorType() { - list($errorType) = explode(' ', $this->getMessage(), 2); + [$errorType] = explode(' ', $this->getMessage(), 2); return $errorType; } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Response/Status.php b/plugins/cache-redis/Predis/Response/Status.php similarity index 92% rename from snappymail/v/0.0.0/app/libraries/Predis/Response/Status.php rename to plugins/cache-redis/Predis/Response/Status.php index 729bb6635..80cab9300 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Response/Status.php +++ b/plugins/cache-redis/Predis/Response/Status.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,8 +14,6 @@ namespace Predis\Response; /** * Represents a status response returned by Redis. - * - * @author Daniele Alessandri */ class Status implements ResponseInterface { @@ -59,7 +58,7 @@ class Status implements ResponseInterface * * @param string $payload Status response payload. * - * @return string + * @return self */ public static function get($payload) { diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Session/Handler.php b/plugins/cache-redis/Predis/Session/Handler.php similarity index 77% rename from snappymail/v/0.0.0/app/libraries/Predis/Session/Handler.php rename to plugins/cache-redis/Predis/Session/Handler.php index d7509c7af..68c87378d 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Session/Handler.php +++ b/plugins/cache-redis/Predis/Session/Handler.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,6 +13,8 @@ namespace Predis\Session; use Predis\ClientInterface; +use ReturnTypeWillChange; +use SessionHandlerInterface; /** * Session handler class that relies on Predis\Client to store PHP's sessions @@ -20,10 +23,8 @@ use Predis\ClientInterface; * This class is mostly intended for PHP 5.4 but it can be used under PHP 5.3 * provided that a polyfill for `SessionHandlerInterface` is defined by either * you or an external package such as `symfony/http-foundation`. - * - * @author Daniele Alessandri */ -class Handler implements \SessionHandlerInterface +class Handler implements SessionHandlerInterface { protected $client; protected $ttl; @@ -32,7 +33,7 @@ class Handler implements \SessionHandlerInterface * @param ClientInterface $client Fully initialized client instance. * @param array $options Session handler options. */ - public function __construct(ClientInterface $client, array $options = array()) + public function __construct(ClientInterface $client, array $options = []) { $this->client = $client; @@ -52,8 +53,11 @@ class Handler implements \SessionHandlerInterface } /** - * {@inheritdoc} + * @param string $save_path + * @param string $session_id + * @return bool */ + #[ReturnTypeWillChange] public function open($save_path, $session_id) { // NOOP @@ -61,8 +65,9 @@ class Handler implements \SessionHandlerInterface } /** - * {@inheritdoc} + * @return bool */ + #[ReturnTypeWillChange] public function close() { // NOOP @@ -70,8 +75,10 @@ class Handler implements \SessionHandlerInterface } /** - * {@inheritdoc} + * @param int $maxlifetime + * @return bool */ + #[ReturnTypeWillChange] public function gc($maxlifetime) { // NOOP @@ -79,8 +86,10 @@ class Handler implements \SessionHandlerInterface } /** - * {@inheritdoc} + * @param string $session_id + * @return string */ + #[ReturnTypeWillChange] public function read($session_id) { if ($data = $this->client->get($session_id)) { @@ -89,9 +98,13 @@ class Handler implements \SessionHandlerInterface return ''; } + /** - * {@inheritdoc} + * @param string $session_id + * @param string $session_data + * @return bool */ + #[ReturnTypeWillChange] public function write($session_id, $session_data) { $this->client->setex($session_id, $this->ttl, $session_data); @@ -100,8 +113,10 @@ class Handler implements \SessionHandlerInterface } /** - * {@inheritdoc} + * @param string $session_id + * @return bool */ + #[ReturnTypeWillChange] public function destroy($session_id) { $this->client->del($session_id); diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Transaction/AbortedMultiExecException.php b/plugins/cache-redis/Predis/Transaction/AbortedMultiExecException.php similarity index 84% rename from snappymail/v/0.0.0/app/libraries/Predis/Transaction/AbortedMultiExecException.php rename to plugins/cache-redis/Predis/Transaction/AbortedMultiExecException.php index b36f38aac..75fc0bb07 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Transaction/AbortedMultiExecException.php +++ b/plugins/cache-redis/Predis/Transaction/AbortedMultiExecException.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,8 +16,6 @@ use Predis\PredisException; /** * Exception class that identifies a MULTI / EXEC transaction aborted by Redis. - * - * @author Daniele Alessandri */ class AbortedMultiExecException extends PredisException { @@ -27,9 +26,10 @@ class AbortedMultiExecException extends PredisException * @param string $message Error message. * @param int $code Error code. */ - public function __construct(MultiExec $transaction, $message, $code = null) + public function __construct(MultiExec $transaction, $message, $code = 0) { - parent::__construct($message, $code); + parent::__construct($message, is_null($code) ? 0 : $code); + $this->transaction = $transaction; } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Transaction/MultiExec.php b/plugins/cache-redis/Predis/Transaction/MultiExec.php similarity index 81% rename from snappymail/v/0.0.0/app/libraries/Predis/Transaction/MultiExec.php rename to plugins/cache-redis/Predis/Transaction/MultiExec.php index 0cf1962da..32ac1e123 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Transaction/MultiExec.php +++ b/plugins/cache-redis/Predis/Transaction/MultiExec.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,24 +12,29 @@ namespace Predis\Transaction; +use Exception; +use InvalidArgumentException; use Predis\ClientContextInterface; use Predis\ClientException; use Predis\ClientInterface; use Predis\Command\CommandInterface; use Predis\CommunicationException; -use Predis\Connection\AggregateConnectionInterface; +use Predis\Connection\Cluster\ClusterInterface; +use Predis\Connection\RelayConnection; use Predis\NotSupportedException; use Predis\Protocol\ProtocolException; +use Predis\Response\Error; use Predis\Response\ErrorInterface as ErrorResponseInterface; use Predis\Response\ServerException; use Predis\Response\Status as StatusResponse; +use Relay\Exception as RelayException; +use Relay\Relay; +use SplQueue; /** * Client-side abstraction of a Redis transaction based on MULTI / EXEC. * * {@inheritdoc} - * - * @author Daniele Alessandri */ class MultiExec implements ClientContextInterface { @@ -38,7 +44,7 @@ class MultiExec implements ClientContextInterface protected $commands; protected $exceptions = true; protected $attempts = 0; - protected $watchKeys = array(); + protected $watchKeys = []; protected $modeCAS = false; /** @@ -52,7 +58,7 @@ class MultiExec implements ClientContextInterface $this->client = $client; $this->state = new MultiExecState(); - $this->configure($client, $options ?: array()); + $this->configure($client, $options ?: []); $this->reset(); } @@ -66,15 +72,15 @@ class MultiExec implements ClientContextInterface */ private function assertClient(ClientInterface $client) { - if ($client->getConnection() instanceof AggregateConnectionInterface) { + if ($client->getConnection() instanceof ClusterInterface) { throw new NotSupportedException( - 'Cannot initialize a MULTI/EXEC transaction over aggregate connections.' + 'Cannot initialize a MULTI/EXEC transaction over cluster connections.' ); } - if (!$client->getProfile()->supportsCommands(array('MULTI', 'EXEC', 'DISCARD'))) { + if (!$client->getCommandFactory()->supports('MULTI', 'EXEC', 'DISCARD')) { throw new NotSupportedException( - 'The current profile does not support MULTI, EXEC and DISCARD.' + 'MULTI, EXEC and DISCARD are not supported by the current command factory.' ); } } @@ -112,7 +118,7 @@ class MultiExec implements ClientContextInterface protected function reset() { $this->state->reset(); - $this->commands = new \SplQueue(); + $this->commands = new SplQueue(); } /** @@ -168,15 +174,30 @@ class MultiExec implements ClientContextInterface * @param string $commandID Command ID. * @param array $arguments Arguments for the command. * - * @throws ServerException - * * @return mixed + * @throws ServerException */ - protected function call($commandID, array $arguments = array()) + protected function call($commandID, array $arguments = []) { - $response = $this->client->executeCommand( - $this->client->createCommand($commandID, $arguments) - ); + try { + $response = $this->client->executeCommand( + $this->client->createCommand($commandID, $arguments) + ); + } catch (ServerException $exception) { + if (!$this->client->getConnection() instanceof RelayConnection) { + throw $exception; + } + + if (strcasecmp($commandID, 'EXEC') != 0) { + throw $exception; + } + + if (!strpos($exception->getMessage(), 'RELAY_ERR_REDIS')) { + throw $exception; + } + + return null; + } if ($response instanceof ErrorResponseInterface) { throw new ServerException($response->getMessage()); @@ -190,10 +211,9 @@ class MultiExec implements ClientContextInterface * * @param CommandInterface $command Command instance. * + * @return $this|mixed * @throws AbortedMultiExecException * @throws CommunicationException - * - * @return $this|mixed */ public function executeCommand(CommandInterface $command) { @@ -207,6 +227,8 @@ class MultiExec implements ClientContextInterface if ($response instanceof StatusResponse && $response == 'QUEUED') { $this->commands->enqueue($command); + } elseif ($response instanceof Relay) { + $this->commands->enqueue($command); } elseif ($response instanceof ErrorResponseInterface) { throw new AbortedMultiExecException($this, $response->getMessage()); } else { @@ -221,22 +243,21 @@ class MultiExec implements ClientContextInterface * * @param string|array $keys One or more keys. * + * @return mixed * @throws NotSupportedException * @throws ClientException - * - * @return mixed */ public function watch($keys) { - if (!$this->client->getProfile()->supportsCommand('WATCH')) { - throw new NotSupportedException('WATCH is not supported by the current profile.'); + if (!$this->client->getCommandFactory()->supports('WATCH')) { + throw new NotSupportedException('WATCH is not supported by the current command factory.'); } if ($this->state->isWatchAllowed()) { throw new ClientException('Sending WATCH after MULTI is not allowed.'); } - $response = $this->call('WATCH', is_array($keys) ? $keys : array($keys)); + $response = $this->call('WATCH', is_array($keys) ? $keys : [$keys]); $this->state->flag(MultiExecState::WATCH); return $response; @@ -262,20 +283,19 @@ class MultiExec implements ClientContextInterface /** * Executes UNWATCH. * - * @throws NotSupportedException - * * @return MultiExec + * @throws NotSupportedException */ public function unwatch() { - if (!$this->client->getProfile()->supportsCommand('UNWATCH')) { + if (!$this->client->getCommandFactory()->supports('UNWATCH')) { throw new NotSupportedException( - 'UNWATCH is not supported by the current profile.' + 'UNWATCH is not supported by the current command factory.' ); } $this->state->unflag(MultiExecState::WATCH); - $this->__call('UNWATCH', array()); + $this->__call('UNWATCH', []); return $this; } @@ -313,7 +333,7 @@ class MultiExec implements ClientContextInterface * * @param mixed $callable Callback for execution. * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * @throws ClientException */ private function checkBeforeExecution($callable) @@ -326,7 +346,7 @@ class MultiExec implements ClientContextInterface if ($callable) { if (!is_callable($callable)) { - throw new \InvalidArgumentException('The argument must be a callable object.'); + throw new InvalidArgumentException('The argument must be a callable object.'); } if (!$this->commands->isEmpty()) { @@ -350,11 +370,10 @@ class MultiExec implements ClientContextInterface * * @param mixed $callable Optional callback for execution. * + * @return array * @throws CommunicationException * @throws AbortedMultiExecException * @throws ServerException - * - * @return array */ public function execute($callable = null) { @@ -378,7 +397,9 @@ class MultiExec implements ClientContextInterface $execResponse = $this->call('EXEC'); - if ($execResponse === null) { + // The additional `false` check is needed for Relay, + // let's hope it won't break anything + if ($execResponse === null || $execResponse === false) { if ($attempts === 0) { throw new AbortedMultiExecException( $this, 'The current transaction has been aborted by the server.' @@ -393,7 +414,7 @@ class MultiExec implements ClientContextInterface break; } while ($attempts-- > 0); - $response = array(); + $response = []; $commands = $this->commands; $size = count($execResponse); @@ -404,10 +425,20 @@ class MultiExec implements ClientContextInterface for ($i = 0; $i < $size; ++$i) { $cmdResponse = $execResponse[$i]; - if ($cmdResponse instanceof ErrorResponseInterface && $this->exceptions) { + if ($this->exceptions && $cmdResponse instanceof ErrorResponseInterface) { throw new ServerException($cmdResponse->getMessage()); } + if ($cmdResponse instanceof RelayException) { + if ($this->exceptions) { + throw new ServerException($cmdResponse->getMessage(), $cmdResponse->getCode(), $cmdResponse); + } + + $commands->dequeue(); + $response[$i] = new Error($cmdResponse->getMessage()); + continue; + } + $response[$i] = $commands->dequeue()->parseResponse($cmdResponse); } @@ -433,7 +464,7 @@ class MultiExec implements ClientContextInterface // NOOP } catch (ServerException $exception) { // NOOP - } catch (\Exception $exception) { + } catch (Exception $exception) { $this->discard(); } diff --git a/snappymail/v/0.0.0/app/libraries/Predis/Transaction/MultiExecState.php b/plugins/cache-redis/Predis/Transaction/MultiExecState.php similarity index 89% rename from snappymail/v/0.0.0/app/libraries/Predis/Transaction/MultiExecState.php rename to plugins/cache-redis/Predis/Transaction/MultiExecState.php index a0a828529..c9be15c87 100644 --- a/snappymail/v/0.0.0/app/libraries/Predis/Transaction/MultiExecState.php +++ b/plugins/cache-redis/Predis/Transaction/MultiExecState.php @@ -3,7 +3,8 @@ /* * This file is part of the Predis package. * - * (c) Daniele Alessandri + * (c) 2009-2020 Daniele Alessandri + * (c) 2021-2023 Till Krüss * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,22 +14,17 @@ namespace Predis\Transaction; /** * Utility class used to track the state of a MULTI / EXEC transaction. - * - * @author Daniele Alessandri */ class MultiExecState { - const INITIALIZED = 1; // 0b00001 - const INSIDEBLOCK = 2; // 0b00010 - const DISCARDED = 4; // 0b00100 - const CAS = 8; // 0b01000 - const WATCH = 16; // 0b10000 + public const INITIALIZED = 1; // 0b00001 + public const INSIDEBLOCK = 2; // 0b00010 + public const DISCARDED = 4; // 0b00100 + public const CAS = 8; // 0b01000 + public const WATCH = 16; // 0b10000 private $flags; - /** - * - */ public function __construct() { $this->flags = 0; diff --git a/plugins/cache-redis/README.md b/plugins/cache-redis/README.md new file mode 100644 index 000000000..d73754706 --- /dev/null +++ b/plugins/cache-redis/README.md @@ -0,0 +1,466 @@ +# Predis # + +[![Software license][ico-license]](LICENSE) +[![Latest stable][ico-version-stable]][link-releases] +[![Latest development][ico-version-dev]][link-releases] +[![Monthly installs][ico-downloads-monthly]][link-downloads] +[![Build status][ico-build]][link-actions] +[![Coverage Status][ico-coverage]][link-coverage] + +A flexible and feature-complete [Redis](http://redis.io) client for PHP 7.2 and newer. + +More details about this project can be found on the [frequently asked questions](FAQ.md). + + +## Main features ## + +- Support for Redis from __3.0__ to __7.0__. +- Support for clustering using client-side sharding and pluggable keyspace distributors. +- Support for [redis-cluster](http://redis.io/topics/cluster-tutorial) (Redis >= 3.0). +- Support for master-slave replication setups and [redis-sentinel](http://redis.io/topics/sentinel). +- Transparent key prefixing of keys using a customizable prefix strategy. +- Command pipelining on both single nodes and clusters (client-side sharding only). +- Abstraction for Redis transactions (Redis >= 2.0) and CAS operations (Redis >= 2.2). +- Abstraction for Lua scripting (Redis >= 2.6) and automatic switching between `EVALSHA` or `EVAL`. +- Abstraction for `SCAN`, `SSCAN`, `ZSCAN` and `HSCAN` (Redis >= 2.8) based on PHP iterators. +- Connections are established lazily by the client upon the first command and can be persisted. +- Connections can be established via TCP/IP (also TLS/SSL-encrypted) or UNIX domain sockets. +- Support for custom connection classes for providing different network or protocol backends. +- Flexible system for defining custom commands and override the default ones. + + +## How to _install_ and use Predis ## + +This library can be found on [Packagist](http://packagist.org/packages/predis/predis) for an easier +management of projects dependencies using [Composer](http://packagist.org/about-composer). +Compressed archives of each release are [available on GitHub](https://github.com/predis/predis/releases). + +```shell +composer require predis/predis +``` + + +### Loading the library ### + +Predis relies on the autoloading features of PHP to load its files when needed and complies with the +[PSR-4 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md). +Autoloading is handled automatically when dependencies are managed through Composer, but it is also +possible to leverage its own autoloader in projects or scripts lacking any autoload facility: + +```php +// Prepend a base path if Predis is not available in your "include_path". +require 'Predis/Autoloader.php'; + +Predis\Autoloader::register(); +``` + + +### Connecting to Redis ### + +When creating a client instance without passing any connection parameter, Predis assumes `127.0.0.1` +and `6379` as default host and port. The default timeout for the `connect()` operation is 5 seconds: + +```php +$client = new Predis\Client(); +$client->set('foo', 'bar'); +$value = $client->get('foo'); +``` + +Connection parameters can be supplied either in the form of URI strings or named arrays. The latter +is the preferred way to supply parameters, but URI strings can be useful when parameters are read +from non-structured or partially-structured sources: + +```php +// Parameters passed using a named array: +$client = new Predis\Client([ + 'scheme' => 'tcp', + 'host' => '10.0.0.1', + 'port' => 6379, +]); + +// Same set of parameters, passed using an URI string: +$client = new Predis\Client('tcp://10.0.0.1:6379'); +``` + +Password protected servers can be accessed by adding `password` to the parameters set. When ACLs are +enabled on Redis >= 6.0, both `username` and `password` are required for user authentication. + +It is also possible to connect to local instances of Redis using UNIX domain sockets, in this case +the parameters must use the `unix` scheme and specify a path for the socket file: + +```php +$client = new Predis\Client(['scheme' => 'unix', 'path' => '/path/to/redis.sock']); +$client = new Predis\Client('unix:/path/to/redis.sock'); +``` + +The client can leverage TLS/SSL encryption to connect to secured remote Redis instances without the +need to configure an SSL proxy like stunnel. This can be useful when connecting to nodes running on +various cloud hosting providers. Encryption can be enabled with using the `tls` scheme and an array +of suitable [options](http://php.net/manual/context.ssl.php) passed via the `ssl` parameter: + +```php +// Named array of connection parameters: +$client = new Predis\Client([ + 'scheme' => 'tls', + 'ssl' => ['cafile' => 'private.pem', 'verify_peer' => true], +]); + +// Same set of parameters, but using an URI string: +$client = new Predis\Client('tls://127.0.0.1?ssl[cafile]=private.pem&ssl[verify_peer]=1'); +``` + +The connection schemes [`redis`](http://www.iana.org/assignments/uri-schemes/prov/redis) (alias of +`tcp`) and [`rediss`](http://www.iana.org/assignments/uri-schemes/prov/rediss) (alias of `tls`) are +also supported, with the difference that URI strings containing these schemes are parsed following +the rules described on their respective IANA provisional registration documents. + +The actual list of supported connection parameters can vary depending on each connection backend so +it is recommended to refer to their specific documentation or implementation for details. + +Predis can aggregate multiple connections when providing an array of connection parameters and the +appropriate option to instruct the client about how to aggregate them (clustering, replication or a +custom aggregation logic). Named arrays and URI strings can be mixed when providing configurations +for each node: + +```php +$client = new Predis\Client([ + 'tcp://10.0.0.1?alias=first-node', ['host' => '10.0.0.2', 'alias' => 'second-node'], +], [ + 'cluster' => 'predis', +]); +``` + +See the [aggregate connections](#aggregate-connections) section of this document for more details. + +Connections to Redis are lazy meaning that the client connects to a server only if and when needed. +While it is recommended to let the client do its own stuff under the hood, there may be times when +it is still desired to have control of when the connection is opened or closed: this can easily be +achieved by invoking `$client->connect()` and `$client->disconnect()`. Please note that the effect +of these methods on aggregate connections may differ depending on each specific implementation. + + +### Client configuration ### + +Many aspects and behaviors of the client can be configured by passing specific client options to the +second argument of `Predis\Client::__construct()`: + +```php +$client = new Predis\Client($parameters, ['prefix' => 'sample:']); +``` + +Options are managed using a mini DI-alike container and their values can be lazily initialized only +when needed. The client options supported by default in Predis are: + + - `prefix`: prefix string applied to every key found in commands. + - `exceptions`: whether the client should throw or return responses upon Redis errors. + - `connections`: list of connection backends or a connection factory instance. + - `cluster`: specifies a cluster backend (`predis`, `redis` or callable). + - `replication`: specifies a replication backend (`predis`, `sentinel` or callable). + - `aggregate`: configures the client with a custom aggregate connection (callable). + - `parameters`: list of default connection parameters for aggregate connections. + - `commands`: specifies a command factory instance to use through the library. + +Users can also provide custom options with values or callable objects (for lazy initialization) that +are stored in the options container for later use through the library. + + +### Aggregate connections ### + +Aggregate connections are the foundation upon which Predis implements clustering and replication and +they are used to group multiple connections to single Redis nodes and hide the specific logic needed +to handle them properly depending on the context. Aggregate connections usually require an array of +connection parameters along with the appropriate client option when creating a new client instance. + +#### Cluster #### + +Predis can be configured to work in clustering mode with a traditional client-side sharding approach +to create a cluster of independent nodes and distribute the keyspace among them. This approach needs +some sort of external health monitoring of nodes and requires the keyspace to be rebalanced manually +when nodes are added or removed: + +```php +$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3']; +$options = ['cluster' => 'predis']; + +$client = new Predis\Client($parameters); +``` + +Along with Redis 3.0, a new supervised and coordinated type of clustering was introduced in the form +of [redis-cluster](http://redis.io/topics/cluster-tutorial). This kind of approach uses a different +algorithm to distribute the keyspaces, with Redis nodes coordinating themselves by communicating via +a gossip protocol to handle health status, rebalancing, nodes discovery and request redirection. In +order to connect to a cluster managed by redis-cluster, the client requires a list of its nodes (not +necessarily complete since it will automatically discover new nodes if necessary) and the `cluster` +client options set to `redis`: + +```php +$parameters = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3']; +$options = ['cluster' => 'redis']; + +$client = new Predis\Client($parameters, $options); +``` + +#### Replication #### + +The client can be configured to operate in a single master / multiple slaves setup to provide better +service availability. When using replication, Predis recognizes read-only commands and sends them to +a random slave in order to provide some sort of load-balancing and switches to the master as soon as +it detects a command that performs any kind of operation that would end up modifying the keyspace or +the value of a key. Instead of raising a connection error when a slave fails, the client attempts to +fall back to a different slave among the ones provided in the configuration. + +The basic configuration needed to use the client in replication mode requires one Redis server to be +identified as the master (this can be done via connection parameters by setting the `role` parameter +to `master`) and one or more slaves (in this case setting `role` to `slave` for slaves is optional): + +```php +$parameters = ['tcp://10.0.0.1?role=master', 'tcp://10.0.0.2', 'tcp://10.0.0.3']; +$options = ['replication' => 'predis']; + +$client = new Predis\Client($parameters, $options); +``` + +The above configuration has a static list of servers and relies entirely on the client's logic, but +it is possible to rely on [`redis-sentinel`](http://redis.io/topics/sentinel) for a more robust HA +environment with sentinel servers acting as a source of authority for clients for service discovery. +The minimum configuration required by the client to work with redis-sentinel is a list of connection +parameters pointing to a bunch of sentinel instances, the `replication` option set to `sentinel` and +the `service` option set to the name of the service: + +```php +$sentinels = ['tcp://10.0.0.1', 'tcp://10.0.0.2', 'tcp://10.0.0.3']; +$options = ['replication' => 'sentinel', 'service' => 'mymaster']; + +$client = new Predis\Client($sentinels, $options); +``` + +If the master and slave nodes are configured to require an authentication from clients, a password +must be provided via the global `parameters` client option. This option can also be used to specify +a different database index. The client options array would then look like this: + +```php +$options = [ + 'replication' => 'sentinel', + 'service' => 'mymaster', + 'parameters' => [ + 'password' => $secretpassword, + 'database' => 10, + ], +]; +``` + +While Predis is able to distinguish commands performing write and read-only operations, `EVAL` and +`EVALSHA` represent a corner case in which the client switches to the master node because it cannot +tell when a Lua script is safe to be executed on slaves. While this is indeed the default behavior, +when certain Lua scripts do not perform write operations it is possible to provide an hint to tell +the client to stick with slaves for their execution: + +```php +$parameters = ['tcp://10.0.0.1?role=master', 'tcp://10.0.0.2', 'tcp://10.0.0.3']; +$options = ['replication' => function () { + // Set scripts that won't trigger a switch from a slave to the master node. + $strategy = new Predis\Replication\ReplicationStrategy(); + $strategy->setScriptReadOnly($LUA_SCRIPT); + + return new Predis\Connection\Replication\MasterSlaveReplication($strategy); +}]; + +$client = new Predis\Client($parameters, $options); +$client->eval($LUA_SCRIPT, 0); // Sticks to slave using `eval`... +$client->evalsha(sha1($LUA_SCRIPT), 0); // ... and `evalsha`, too. +``` + +The [`examples`](examples/) directory contains a few scripts that demonstrate how the client can be +configured and used to leverage replication in both basic and complex scenarios. + + +### Command pipelines ### + +Pipelining can help with performances when many commands need to be sent to a server by reducing the +latency introduced by network round-trip timings. Pipelining also works with aggregate connections. +The client can execute the pipeline inside a callable block or return a pipeline instance with the +ability to chain commands thanks to its fluent interface: + +```php +// Executes a pipeline inside the given callable block: +$responses = $client->pipeline(function ($pipe) { + for ($i = 0; $i < 1000; $i++) { + $pipe->set("key:$i", str_pad($i, 4, '0', 0)); + $pipe->get("key:$i"); + } +}); + +// Returns a pipeline that can be chained thanks to its fluent interface: +$responses = $client->pipeline()->set('foo', 'bar')->get('foo')->execute(); +``` + + +### Transactions ### + +The client provides an abstraction for Redis transactions based on `MULTI` and `EXEC` with a similar +interface to command pipelines: + +```php +// Executes a transaction inside the given callable block: +$responses = $client->transaction(function ($tx) { + $tx->set('foo', 'bar'); + $tx->get('foo'); +}); + +// Returns a transaction that can be chained thanks to its fluent interface: +$responses = $client->transaction()->set('foo', 'bar')->get('foo')->execute(); +``` + +This abstraction can perform check-and-set operations thanks to `WATCH` and `UNWATCH` and provides +automatic retries of transactions aborted by Redis when `WATCH`ed keys are touched. For an example +of a transaction using CAS you can see [the following example](examples/transaction_using_cas.php). + + +### Adding new commands ### + +While we try to update Predis to stay up to date with all the commands available in Redis, you might +prefer to stick with an old version of the library or provide a different way to filter arguments or +parse responses for specific commands. To achieve that, Predis provides the ability to implement new +command classes to define or override commands in the default command factory used by the client: + +```php +// Define a new command by extending Predis\Command\Command: +class BrandNewRedisCommand extends Predis\Command\Command +{ + public function getId() + { + return 'NEWCMD'; + } +} + +// Inject your command in the current command factory: +$client = new Predis\Client($parameters, [ + 'commands' => [ + 'newcmd' => 'BrandNewRedisCommand', + ], +]); + +$response = $client->newcmd(); +``` + +There is also a method to send raw commands without filtering their arguments or parsing responses. +Users must provide the list of arguments for the command as an array, following the signatures as +defined by the [Redis documentation for commands](http://redis.io/commands): + +```php +$response = $client->executeRaw(['SET', 'foo', 'bar']); +``` + + +### Script commands ### + +While it is possible to leverage [Lua scripting](http://redis.io/commands/eval) on Redis 2.6+ using +directly [`EVAL`](http://redis.io/commands/eval) and [`EVALSHA`](http://redis.io/commands/evalsha), +Predis offers script commands as an higher level abstraction built upon them to make things simple. +Script commands can be registered in the command factory used by the client and are accessible as if +they were plain Redis commands, but they define Lua scripts that get transmitted to the server for +remote execution. Internally they use [`EVALSHA`](http://redis.io/commands/evalsha) by default and +identify a script by its SHA1 hash to save bandwidth, but [`EVAL`](http://redis.io/commands/eval) +is used as a fall back when needed: + +```php +// Define a new script command by extending Predis\Command\ScriptCommand: +class ListPushRandomValue extends Predis\Command\ScriptCommand +{ + public function getKeysCount() + { + return 1; + } + + public function getScript() + { + return << [ + 'lpushrand' => 'ListPushRandomValue', + ], +]); + +$response = $client->lpushrand('random_values', $seed = mt_rand()); +``` + + +### Customizable connection backends ### + +Predis can use different connection backends to connect to Redis. The builtin Relay integration +leverages the [Relay](https://github.com/cachewerk/relay) extension for PHP for major performance +gains, by caching a partial replica of the Redis dataset in PHP shared runtime memory. + +```php +$client = new Predis\Client('tcp://127.0.0.1', [ + 'connections' => 'relay', +]); +``` + +Developers can create their own connection classes to support whole new network backends, extend +existing classes or provide completely different implementations. Connection classes must implement +`Predis\Connection\NodeConnectionInterface` or extend `Predis\Connection\AbstractConnection`: + +```php +class MyConnectionClass implements Predis\Connection\NodeConnectionInterface +{ + // Implementation goes here... +} + +// Use MyConnectionClass to handle connections for the `tcp` scheme: +$client = new Predis\Client('tcp://127.0.0.1', [ + 'connections' => ['tcp' => 'MyConnectionClass'], +]); +``` + +For a more in-depth insight on how to create new connection backends you can refer to the actual +implementation of the standard connection classes available in the `Predis\Connection` namespace. + + +## Development ## + + +### Reporting bugs and contributing code ### + +Contributions to Predis are highly appreciated either in the form of pull requests for new features, +bug fixes, or just bug reports. We only ask you to adhere to issue and pull request templates. + + +### Test suite ### + +__ATTENTION__: Do not ever run the test suite shipped with Predis against instances of Redis running +in production environments or containing data you are interested in! + +Predis has a comprehensive test suite covering every aspect of the library and that can optionally +perform integration tests against a running instance of Redis (required >= 2.4.0 in order to verify +the correct behavior of the implementation of each command. Integration tests for unsupported Redis +commands are automatically skipped. If you do not have Redis up and running, integration tests can +be disabled. See [the tests README](tests/README.md) for more details about testing this library. + +Predis uses GitHub Actions for continuous integration and the history for past and current builds can be +found [on its actions page](https://github.com/predis/predis/actions). + +### License ### + +The code for Predis is distributed under the terms of the MIT license (see [LICENSE](LICENSE)). + +[ico-license]: https://img.shields.io/github/license/predis/predis.svg?style=flat-square +[ico-version-stable]: https://img.shields.io/github/v/tag/predis/predis?label=stable&style=flat-square +[ico-version-dev]: https://img.shields.io/github/v/tag/predis/predis?include_prereleases&label=pre-release&style=flat-square +[ico-downloads-monthly]: https://img.shields.io/packagist/dm/predis/predis.svg?style=flat-square +[ico-build]: https://img.shields.io/github/actions/workflow/status/predis/predis/tests.yml?branch=main&style=flat-square +[ico-coverage]: https://img.shields.io/coverallsCoverage/github/predis/predis?style=flat-square + +[link-releases]: https://github.com/predis/predis/releases +[link-actions]: https://github.com/predis/predis/actions +[link-downloads]: https://packagist.org/packages/predis/predis/stats +[link-coverage]: https://coveralls.io/github/predis/predis diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Redis.php b/plugins/cache-redis/Redis.php similarity index 80% rename from snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Redis.php rename to plugins/cache-redis/Redis.php index 9271f0cb0..ea41a148c 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/Redis.php +++ b/plugins/cache-redis/Redis.php @@ -27,7 +27,7 @@ class Redis implements \MailSo\Cache\DriverInterface private string $sKeyPrefix; - function __construct(string $sHost = '127.0.0.1', int $iPort = 6379, int $iExpire = 43200, string $sKeyPrefix = '') + function __construct(string $sHost = '127.0.0.1', int $iPort = 6379, int $iExpire = 43200) { $this->iExpire = 0 < $iExpire ? $iExpire : 43200; @@ -51,10 +51,14 @@ class Redis implements \MailSo\Cache\DriverInterface $this->oRedis = null; unset($oExc); } + } + public function setPrefix(string $sKeyPrefix) : void + { + $sKeyPrefix = \rtrim(\trim($sKeyPrefix), '\\/'); $this->sKeyPrefix = empty($sKeyPrefix) ? $sKeyPrefix - : \preg_replace('/[^a-zA-Z0-9_]/', '_', rtrim(trim($sKeyPrefix), '\\/')) . '/'; + : \preg_replace('/[^a-zA-Z0-9_]/', '_', $sKeyPrefix).'/'; } public function Set(string $sKey, string $sValue) : bool @@ -68,10 +72,15 @@ class Redis implements \MailSo\Cache\DriverInterface return $sValue === true || $sValue == 'OK'; } - public function Get(string $sKey) : string + public function Exists(string $sKey) : bool + { + return $this->oRedis && $this->oRedis->exists($this->generateCachedKey($sKey)); + } + + public function Get(string $sKey) : ?string { $sValue = $this->oRedis ? $this->oRedis->get($this->generateCachedKey($sKey)) : ''; - return \is_string($sValue) ? $sValue : ''; + return \is_string($sValue) ? $sValue : null; } public function Delete(string $sKey) : void diff --git a/plugins/cache-redis/index.php b/plugins/cache-redis/index.php new file mode 100644 index 000000000..8387ebb75 --- /dev/null +++ b/plugins/cache-redis/index.php @@ -0,0 +1,58 @@ +addHook('main.fabrica', 'MainFabrica'); + } + } + + public function Supported() : string + { + return ''; + } + + public function MainFabrica($sName, &$mResult) + { + if ('cache' == $sName) { + require_once __DIR__ . '/Redis.php'; + $mResult = new \MailSo\Cache\Drivers\Redis( + $this->Config()->Get('plugin', 'host', '127.0.0.1'), + (int) $this->Config()->Get('plugin', 'port', 6379) + ); + } + } + + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('host')->SetLabel('Host') + ->SetDescription('Hostname of the redis server') + ->SetDefaultValue('127.0.0.1'), + \RainLoop\Plugins\Property::NewInstance('port')->SetLabel('Port') + ->SetDescription('Port of the redis server') + ->SetDefaultValue(6379) +/* + ,\RainLoop\Plugins\Property::NewInstance('password')->SetLabel('Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) + ->SetDefaultValue('') +*/ + ); + } +} diff --git a/plugins/change-password-cpanel/driver.php b/plugins/change-password-cpanel/driver.php new file mode 100644 index 000000000..61d8d222a --- /dev/null +++ b/plugins/change-password-cpanel/driver.php @@ -0,0 +1,121 @@ +oConfig = $oConfig; + $this->oLogger = $oLogger; + } + + public static function isSupported() : bool + { + return !empty($_ENV['CPANEL']) && \is_readable('/usr/local/cpanel/php/cpanel.php'); + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('cpanel_host')->SetLabel('cPanel Host') + ->SetDefaultValue('127.0.0.1'), + \RainLoop\Plugins\Property::NewInstance('cpanel_port')->SetLabel('cPanel Port') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) + ->SetDefaultValue(2087), + \RainLoop\Plugins\Property::NewInstance('cpanel_ssl')->SetLabel('Use SSL') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(false), + \RainLoop\Plugins\Property::NewInstance('cpanel_user')->SetLabel('cPanel User') + ->SetDefaultValue(''), + \RainLoop\Plugins\Property::NewInstance('cpanel_pass')->SetLabel('cPanel Password') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) + ->SetDefaultValue(''), + \RainLoop\Plugins\Property::NewInstance('cpanel_allowed_emails')->SetLabel('Allowed emails') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net') + ->SetDefaultValue('*') + ); + } + + public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool + { + if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'cpanel_allowed_emails', ''))) { + return false; + } + + $this->oLogger->Write('CPANEL: Try to change password for '.$oAccount->Email()); + + if (!\class_exists('cPanel\\jsonapi')) { + require_once __DIR__ . '/jsonapi.php'; + } + + $sHost = $this->oConfig->Get('plugin', 'cpanel_host', '127.0.0.1'); + $iPort = $this->oConfig->Get('plugin', 'cpanel_port', 2087); + $sUser = $this->oConfig->Get('plugin', 'cpanel_user', ''); + $sPassword = $this->oConfig->Get('plugin', 'cpanel_pass', ''); + + if (empty($sHost) || 1 > $iPort || !\strlen($sUser) || !\strlen($sPassword)) { + $this->oLogger->Write('CPANEL: Incorrent configuration data', \MailSo\Log\Enumerations\Type::ERROR); + return false; + } + + $sEmail = $oAccount->Email(); + $sEmailUser = \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail); + $sEmailDomain = \MailSo\Base\Utils::getEmailAddressDomain($sEmail); + + $sHost = \str_replace('{user:domain}', $sEmailDomain, $sHost); + $sUser = \str_replace('{user:email}', $sEmail, $sUser); + $sUser = \str_replace('{user:login}', $sEmailUser, $sUser); + $sPassword = \str_replace('{user:password}', (string) $oPrevPassword, $sPassword); + + $bResult = false; + try + { + $oJSONApi = new \cPanel\jsonapi($sHost); + $oJSONApi->set_port($iPort); + $oJSONApi->set_protocol($this->oConfig->Get('plugin', 'cpanel_ssl', false) ? 'https' : 'http'); + $oJSONApi->set_debug(false); +// $oJSONApi->set_http_client('fopen'); +// $oJSONApi->set_http_client('curl'); + $oJSONApi->password_auth($sUser, $sPassword); + + $aArgs = array( + 'email' => $sEmailUser, + 'domain' => $sEmailDomain, + 'password' => $sNewPassword + ); + + $sResult = $oJSONApi->api2_query($sUser, 'Email', 'passwdpop', $aArgs); + if ($sResult) { + $this->oLogger->Write('CPANEL: '.$sResult, \MailSo\Log\Enumerations\Type::INFO); + + $aResult = \json_decode($sResult, true); + $bResult = isset($aResult['cpanelresult']['data'][0]['result']) && + !!$aResult['cpanelresult']['data'][0]['result']; + } + + if (!$bResult) { + $this->oLogger->Write('CPANEL: '.$sResult, \MailSo\Log\Enumerations\Type::ERROR); + } + } + catch (\Exception $oException) + { + $this->oLogger->WriteException($oException); + } + + return $bResult; + } +} diff --git a/plugins/change-password-cpanel/index.php b/plugins/change-password-cpanel/index.php new file mode 100644 index 000000000..a3ee6138d --- /dev/null +++ b/plugins/change-password-cpanel/index.php @@ -0,0 +1,17 @@ +user = $user; + } + + if ($password != null) { + $this->set_password($password); + } + + $this->host = $host; + + // Detemine what the default http client should be. + if ( \function_exists('curl_setopt') ) { + $this->http_client = "curl"; + } elseif ( \ini_get('allow_url_fopen') ) { + $this->http_client = "fopen"; + } else { + throw new \Exception('allow_url_fopen and curl are neither available in this PHP configuration'); + } + + } + + public function set_debug( bool $debug = true ) + { + $this->debug = $debug; + } + + public function set_host( string $host ) + { + $this->host = $host; + } + + public function set_port( int $port ) + { + if ($port < 1 || $port > 65535) { + throw new \Exception('non integer or negative integer passed to set_port'); + } + + // Account for ports that are non-ssl + if ($port == '2086' || $port == '2082' || $port == '80' || $port == '2095') { + $this->set_protocol('http'); + } + + $this->port = $port; + } + + public function set_protocol( string $proto ) + { + if ($proto != 'https' && $proto != 'http') { + throw new \Exception('https and http are the only protocols that can be passed to set_protocol'); + } + $this->protocol = $proto; + } + + public function set_password( string $pass ) + { + $this->auth_type = 'pass'; + $this->auth = $pass; + } + + public function set_hash( string $hash ) + { + $this->auth_type = 'hash'; + $this->auth = \preg_replace("/(\n|\r|\s)/", '', $hash); + } + + public function hash_auth( string $user, string $hash ) + { + $this->set_hash( $hash ); + $this->user = $user; + } + + public function password_auth( string $user, string $pass ) + { + $this->set_password( $pass ); + $this->user = $user; + } + + public function set_http_client( string $client ) + { + if ( ( $client != 'curl' ) && ( $client != 'fopen' ) ) { + throw new \Exception('only curl and fopen and allowed http clients'); + } + $this->http_client = $client; + } + + /** + * Perform an XML-API Query + * + * This function will perform an XML-API Query and return the specified output format of the call being made + * + * @param string $function The XML-API call to execute + * @param array $vars An associative array of the parameters to be passed to the XML-API Calls + * @return mixed + */ + public function jsonapi_query( string $function, array $vars = array() ) + { + // Check to make sure all the data needed to perform the query is in place + if (!$function) { + throw new \Exception('jsonapi_query() requires a function to be passed to it'); + } + + if ($this->user == null) { + throw new \Exception('no user has been set'); + } + + if ($this->auth ==null) { + throw new \Exception('no authentication information has been set'); + } + + // Build the query: + + $query_type = '/json-api/'; + + $args = \http_build_query($vars, '', '&'); + $url = $this->protocol . '://' . $this->host . ':' . $this->port . $query_type . $function; + + if ($this->debug) { + \error_log('URL: ' . $url); + \error_log('DATA: ' . $args); + } + + // Set the $auth string + + $authstr = ''; + if ($this->auth_type == 'hash') { + $authstr = 'Authorization: WHM ' . $this->user . ':' . $this->auth . "\r\n"; + } elseif ($this->auth_type == 'pass') { + $authstr = 'Authorization: Basic ' . \base64_encode($this->user .':'. $this->auth) . "\r\n"; + } else { + throw new \Exception('invalid auth_type set'); + } + + if ($this->debug) { + \error_log("Authentication Header: " . $authstr ."\n"); + } + + // Perform the query (or pass the info to the functions that actually do perform the query) + + $response = ''; + if ($this->http_client == 'curl') { + $response = $this->curl_query($url, $args, $authstr); + } elseif ($this->http_client == 'fopen') { + $response = $this->fopen_query($url, $args, $authstr); + } + + // fix #1 + $aMatch = array(); + if ($response && false !== stripos($response, '') && + \preg_match('/HTTP-EQUIV[\s]?=[\s]?"refresh"/i', $response) && + \preg_match('/]+url[\s]?=[\s]?([^">]+)/i', $response, $aMatch) && + !empty($aMatch[1]) && 0 === \strpos(\trim($aMatch[1]), 'http')) + { + $url = \trim($aMatch[1]) . $query_type . $function; + if ($this->debug) { + \error_log('new URL: ' . $url); + } + + if ($this->http_client == 'curl') { + $response = $this->curl_query($url, $args, $authstr); + } elseif ($this->http_client == 'fopen') { + $response = $this->fopen_query($url, $args, $authstr); + } + } + // --- + + /* + * Post-Query Block + * Handle response, return proper data types, debug, etc + */ + + // print out the response if debug mode is enabled. + if ($this->debug) { + \error_log("RESPONSE:\n " . $response); + } + + // The only time a response should contain is in the case of authentication error + // cPanel 11.25 fixes this issue, but if is in the response, we'll error out. + + if (\stristr($response, '') == true) { + if (\stristr($response, 'Login Attempt Failed') == true) { + \error_log("Login Attempt Failed"); + + return; + } + if (\stristr($response, 'action="/login/"') == true) { + \error_log("Authentication Error"); + + return; + } + + return; + } + + return $response; + } + + private function curl_query( $url, $postdata, $authstr ) + { + $curl = \curl_init(); + \curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + // Return contents of transfer on curl_exec + \curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + // Allow self-signed certs + \curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); + // Set the URL + \curl_setopt($curl, CURLOPT_URL, $url); + // Increase buffer size to avoid "funny output" exception + \curl_setopt($curl, CURLOPT_BUFFERSIZE, 131072); + + // Pass authentication header + $header[0] =$authstr . + "Content-Type: application/x-www-form-urlencoded\r\n" . + "Content-Length: " . strlen($postdata) . "\r\n" . "\r\n" . $postdata; + + \curl_setopt($curl, CURLOPT_HTTPHEADER, $header); + + \curl_setopt($curl, CURLOPT_POST, 1); + + $result = \curl_exec($curl); + if ($result == false) { + throw new \Exception("curl_exec threw error \"" . \curl_error($curl) . "\" for " . $url . "?" . $postdata ); + } + \curl_close($curl); + + return $result; + } + + private function fopen_query( $url, $postdata, $authstr ) + { + if ( !(ini_get('allow_url_fopen') ) ) { + throw new \Exception('fopen_query called on system without allow_url_fopen enabled in php.ini'); + } + + $opts = array( + 'http' => array( + 'allow_self_signed' => true, + 'method' => 'POST', + 'header' => $authstr . + "Content-Type: application/x-www-form-urlencoded\r\n" . + "Content-Length: " . strlen($postdata) . "\r\n" . + "\r\n" . $postdata + ) + ); + $context = \stream_context_create($opts); + + return \file_get_contents($url, false, $context); + } + + public function api2_query($user, $module, $function, $args = array()) + { + if (!isset($user) || !isset($module) || !isset($function) ) { + \error_log("api2_query requires that a username, module and function are passed to it"); + + return false; + } + if (!is_array($args)) { + \error_log("api2_query requires that an array is passed to it as the 4th parameter"); + + return false; + } + + $args['cpanel_jsonapi_user'] = $user; + $args['cpanel_jsonapi_module'] = $module; + $args['cpanel_jsonapi_func'] = $function; + $args['cpanel_jsonapi_apiversion'] = '2'; + + return $this->jsonapi_query('cpanel', $args); + } +} diff --git a/plugins/change-password-froxlor/driver.php b/plugins/change-password-froxlor/driver.php index 0e26d32c3..f7194edd8 100644 --- a/plugins/change-password-froxlor/driver.php +++ b/plugins/change-password-froxlor/driver.php @@ -1,5 +1,7 @@ Email(), $this->oConfig->Get('plugin', 'froxlor_allowed_emails', ''))) { return false; @@ -72,12 +74,12 @@ class ChangePasswordFroxlorDriver if (!empty($aFetchResult['id'])) { $sDbPassword = $aFetchResult['password_enc']; $sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$')); - if (\crypt($sPrevPassword, $sDbSalt) === $sDbPassword) { + if (\crypt($oPrevPassword, $sDbSalt) === $sDbPassword) { $oStmt = $oPdo->prepare('UPDATE mail_users SET password_enc = ? WHERE id = ?'); return !!$oStmt->execute(array( - $this->cryptPassword($sNewPassword), + $this->cryptPassword($oNewPassword), $aFetchResult['id'] )); } @@ -93,7 +95,7 @@ class ChangePasswordFroxlorDriver return false; } - private function cryptPassword(string $sPassword) : string + private function cryptPassword(SensitiveString $oPassword) : string { if (\defined('CRYPT_SHA512') && CRYPT_SHA512) { $sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$'; @@ -102,6 +104,6 @@ class ChangePasswordFroxlorDriver } else { $sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$'; } - return \crypt($sPassword, $sSalt); + return \crypt($oPassword, $sSalt); } } diff --git a/plugins/change-password-froxlor/index.php b/plugins/change-password-froxlor/index.php index 58ace2d53..339d7e937 100644 --- a/plugins/change-password-froxlor/index.php +++ b/plugins/change-password-froxlor/index.php @@ -1,15 +1,13 @@ Email(), $this->oConfig->Get('plugin', 'hestia_allowed_emails', ''))) { return false; @@ -53,8 +55,8 @@ class ChangePasswordHestiaDriver $HTTP = \SnappyMail\HTTP\Request::factory(); $postvars = array( 'email' => $oAccount->Email(), - 'password' => $sPrevPassword, - 'new' => $sNewPassword, + 'password' => (string) $oPrevPassword, + 'new' => (string) $oNewPassword, ); $response = $HTTP->doRequest('POST', 'https://'.$sHost.':'.$sPort.'/reset/mail/', \http_build_query($postvars)); if (!$response) { diff --git a/plugins/change-password-hestia/index.php b/plugins/change-password-hestia/index.php index d19d30667..2decd1548 100644 --- a/plugins/change-password-hestia/index.php +++ b/plugins/change-password-hestia/index.php @@ -1,15 +1,13 @@ Email(), $this->oConfig->Get('plugin', 'hmailserver_emails', ''))) { return false; @@ -57,12 +58,12 @@ class ChangePasswordHMailServerDriver $this->oConfig->Get('plugin', 'hmailserver_password', '') )) { $sEmail = $oAccount->Email(); - $sDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail); + $sDomain = \MailSo\Base\Utils::getEmailAddressDomain($sEmail); $oHmailDomain = $oHmailApp->Domains->ItemByName($sDomain); if ($oHmailDomain) { $oHmailAccount = $oHmailDomain->Accounts->ItemByAddress($sEmail); if ($oHmailAccount) { - $oHmailAccount->Password = $sNewPassword; + $oHmailAccount->Password = (string) $oNewPassword; $oHmailAccount->Save(); $bResult = true; } else { diff --git a/plugins/change-password-hmailserver/index.php b/plugins/change-password-hmailserver/index.php index eb8e1cd05..8fa5180d8 100644 --- a/plugins/change-password-hmailserver/index.php +++ b/plugins/change-password-hmailserver/index.php @@ -1,14 +1,12 @@ Email(), $this->oConfig->Get('plugin', 'ispconfig_allowed_emails', ''))) { return false; @@ -71,10 +73,10 @@ class ChangePasswordISPConfigDriver if (!empty($aFetchResult['mailuser_id'])) { $sDbPassword = $aFetchResult['password']; $sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$')); - if (\crypt($sPrevPassword, $sDbSalt) === $sDbPassword) { + if (\crypt($oPrevPassword, $sDbSalt) === $sDbPassword) { $oStmt = $oPdo->prepare('UPDATE mail_user SET password = ? WHERE mailuser_id = ?'); return !!$oStmt->execute(array( - $this->cryptPassword($sNewPassword), + $this->cryptPassword($oNewPassword), $aFetchResult['mailuser_id'] )); } @@ -90,7 +92,7 @@ class ChangePasswordISPConfigDriver return false; } - private function cryptPassword(string $sPassword) : string + private function cryptPassword(SensitiveString $oPassword) : string { if (\defined('CRYPT_SHA512') && CRYPT_SHA512) { $sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$'; @@ -99,6 +101,6 @@ class ChangePasswordISPConfigDriver } else { $sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$'; } - return \crypt($sPassword, $sSalt); + return \crypt($oPassword, $sSalt); } } diff --git a/plugins/change-password-ispconfig/index.php b/plugins/change-password-ispconfig/index.php index ade0ecb09..5486e41ae 100644 --- a/plugins/change-password-ispconfig/index.php +++ b/plugins/change-password-ispconfig/index.php @@ -1,14 +1,12 @@ oLogger = $oLogger; + $this->sHostName = $oConfig->Get('plugin', 'mailcow_api_hostname', ''); + $this->sApiToken = $oConfig->Get('plugin', 'mailcow_api_token', ''); + } + + public static function isSupported() : bool + { + return true; + } + + public static function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('mailcow_api_hostname') + ->SetLabel('Mailcow API hostname'), + \RainLoop\Plugins\Property::NewInstance('mailcow_api_token') + ->SetLabel('API token') + ->SetDescription('The Read/Write API token'), + ); + } + + public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool + { + $url = 'https://'.$this->sHostName.'/api/v1/edit/mailbox'; + $headers = [ + 'content-type' => 'application/json', + 'accept' => 'application/json', + 'X-API-Key' => $this->sApiToken, + ]; + $body = array( + 'items' => [ $oAccount->Email() ], + 'attr' => [ + 'password' => (string)$sNewPassword, + 'password2' => (string)$sNewPassword, + ], + ); + + $ch = curl_init($url); + $payload = json_encode($body); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_HTTPHEADER, array_map(function($k, $v) {return "$k: $v";}, array_keys($headers), $headers)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $result = curl_exec($ch); + curl_close($ch); + + $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + + if ($status === 200 && $result && ($res = json_decode($result, true)) && $res[0]['type'] === 'success') { + return true; + } + + $this->oLogger->Write("Mailcow[Error]: Response: {$status} {$result}"); + return false; + } +} diff --git a/plugins/change-password-mailcow/index.php b/plugins/change-password-mailcow/index.php new file mode 100644 index 000000000..80a19c406 --- /dev/null +++ b/plugins/change-password-mailcow/index.php @@ -0,0 +1,18 @@ +Email(), $this->oConfig->Get('plugin', 'poppassd_allowed_emails', ''))) { return false; @@ -55,7 +56,7 @@ class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient try { $this->sendRequestWithCheck('user', $oAccount->IncLogin(), true); - $this->sendRequestWithCheck('pass', $sPrevPassword, true); + $this->sendRequestWithCheck('pass', $oPrevPassword, true); } catch (\Throwable $oException) { @@ -65,7 +66,7 @@ class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient $this->bIsLoggined = true; if ($this->bIsLoggined) { - $this->sendRequestWithCheck('newpass', $sNewPassword); + $this->sendRequestWithCheck('newpass', $oNewPassword); } else { $this->writeLogException( new \RuntimeException('Required login'), @@ -152,13 +153,13 @@ class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient private function validateResponse(bool $bAuthRequestValidate = false) : self { - $this->getNextBuffer(); + $sResponseBuffer = $this->getNextBuffer(); - $bResult = \preg_match($bAuthRequestValidate ? '/^[23]\d\d/' : '/^2\d\d/', trim($this->sResponseBuffer)); + $bResult = \preg_match($bAuthRequestValidate ? '/^[23]\d\d/' : '/^2\d\d/', \trim($sResponseBuffer)); if (!$bResult) { // POP3 validation hack - $bResult = '+OK ' === \substr(\trim($this->sResponseBuffer), 0, 4); + $bResult = '+OK ' === \substr(\trim($sResponseBuffer), 0, 4); } if (!$bResult) { diff --git a/plugins/change-password-poppassd/index.php b/plugins/change-password-poppassd/index.php index a66c13f79..cad4573ec 100644 --- a/plugins/change-password-poppassd/index.php +++ b/plugins/change-password-poppassd/index.php @@ -1,14 +1,12 @@ Email()); + $sDomain = \MailSo\Base\Utils::getEmailAddressDomain($oAccount->Email()); $sUserDn = \strtr($this->sUserDnFormat, array( '{domain}' => $sDomain, '{domain:dc}' => 'dc='.\strtr($sDomain, array('.' => ',dc=')), '{email}' => $oAccount->Email(), - '{email:user}' => \MailSo\Base\Utils::GetAccountNameFromEmail($oAccount->Email()), + '{email:user}' => \MailSo\Base\Utils::getEmailAddressLocalPart($oAccount->Email()), '{email:domain}' => $sDomain, '{login}' => $oAccount->IncLogin(), '{imap:login}' => $oAccount->IncLogin(), - '{imap:host}' => $oAccount->Domain()->IncHost(), - '{imap:port}' => $oAccount->Domain()->IncPort(), + '{imap:host}' => $oAccount->Domain()->ImapSettings()->host, + '{imap:port}' => $oAccount->Domain()->ImapSettings()->port, '{gecos}' => \function_exists('posix_getpwnam') ? \posix_getpwnam($oAccount->IncLogin()) : '' )); @@ -89,25 +91,25 @@ class ChangePasswordDriverLDAP throw new \Exception('ldap_start_tls error '.\ldap_errno($oCon).': '.\ldap_error($oCon)); } - if (!\ldap_bind($oCon, $sUserDn, $sPrevPassword)) { + if (!\ldap_bind($oCon, $sUserDn, $oPrevPassword)) { throw new \Exception('ldap_bind error '.\ldap_errno($oCon).': '.\ldap_error($oCon)); } $sSshaSalt = ''; $sPrefix = '{'.\strtoupper($this->sPasswordEncType).'}'; - $sEncodedNewPassword = $sNewPassword; + $sEncodedNewPassword = $oNewPassword; switch ($sPrefix) { case '{SSHA}': $sSshaSalt = $this->getSalt(4); case '{SHA}': - $sEncodedNewPassword = $sPrefix.\base64_encode(\hash('sha1', $sNewPassword.$sSshaSalt, true).$sSshaSalt); + $sEncodedNewPassword = $sPrefix.\base64_encode(\hash('sha1', $oNewPassword.$sSshaSalt, true).$sSshaSalt); break; case '{MD5}': - $sEncodedNewPassword = $sPrefix.\base64_encode(\md5($sNewPassword, true)); + $sEncodedNewPassword = $sPrefix.\base64_encode(\md5($oNewPassword, true)); break; case '{CRYPT}': - $sEncodedNewPassword = $sPrefix.\crypt($sNewPassword, $this->getSalt(2)); + $sEncodedNewPassword = $sPrefix.\crypt($oNewPassword, $this->getSalt(2)); break; } diff --git a/plugins/change-password/drivers/pdo.php b/plugins/change-password/drivers/pdo.php index 634085188..2051ed2ce 100644 --- a/plugins/change-password/drivers/pdo.php +++ b/plugins/change-password/drivers/pdo.php @@ -1,5 +1,7 @@ $sEmail, - ':oldpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sPrevPassword), - ':newpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sNewPassword), - ':domain' => \MailSo\Base\Utils::GetDomainFromEmail($sEmail), - ':username' => \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail), + ':oldpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $oPrevPassword), + ':newpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $oNewPassword), + ':domain' => \MailSo\Base\Utils::getEmailAddressDomain($sEmail), + ':username' => \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail), ':login_name' => $oAccount->IncLogin() ); diff --git a/plugins/change-password/index.php b/plugins/change-password/index.php index 3b1bc4afd..d0d49bab7 100644 --- a/plugins/change-password/index.php +++ b/plugins/change-password/index.php @@ -1,14 +1,15 @@ SetLabel('Password minimum length') ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) ->SetDescription('Minimum length of the password') ->SetDefaultValue(10) ->SetAllowedInJs(true), - \RainLoop\Plugins\Property::NewInstance("pass_min_strength") + \RainLoop\Plugins\Property::NewInstance('pass_min_strength') ->SetLabel('Password minimum strength') ->SetType(\RainLoop\Enumerations\PluginPropertyType::INT) ->SetDescription('Minimum strength of the password in %') ->SetDefaultValue(70) ->SetAllowedInJs(true), + \RainLoop\Plugins\Property::NewInstance('check_hibp') + ->SetLabel('Check Have I Been Pwned') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDescription('Check if new passphrase is in a data breach') + ->SetDefaultValue(false), ]; foreach ($this->getSupportedDrivers(true) as $name => $class) { $group = new \RainLoop\Plugins\PropertyCollection($name); @@ -149,15 +156,19 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin if ($sPrevPassword !== $oAccount->IncPassword()) { throw new ClientException(static::CurrentPasswordIncorrect, null, $oActions->StaticI18N('NOTIFICATIONS/CURRENT_PASSWORD_INCORRECT')); } + $oPrevPassword = new \SnappyMail\SensitiveString($sPrevPassword); $sNewPassword = $this->jsonParam('NewPassword'); if ($this->Config()->Get('plugin', 'pass_min_length', 10) > \strlen($sNewPassword)) { throw new ClientException(static::NewPasswordShort, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_SHORT')); } - if ($this->Config()->Get('plugin', 'pass_min_strength', 70) > static::PasswordStrength($sNewPassword)) { throw new ClientException(static::NewPasswordWeak, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_WEAK')); } + $oNewPassword = new \SnappyMail\SensitiveString($sNewPassword); + if ($this->Config()->Get('plugin', 'check_hibp', false) && \SnappyMail\Hibp::password($oNewPassword)) { + throw new ClientException(static::NewPasswordHibp, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_HIBP')); + } $bResult = false; $oConfig = $this->Config(); @@ -171,7 +182,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin $oConfig, $oLogger ); - if (!$oDriver->ChangePassword($oAccount, $sPrevPassword, $sNewPassword)) { + if (!$oDriver->ChangePassword($oAccount, $oPrevPassword, $oNewPassword)) { throw new ClientException(static::CouldNotSaveNewPassword); } $bResult = true; @@ -196,15 +207,16 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin throw new ClientException(static::CouldNotSaveNewPassword); } - $oAccount->SetPassword($sNewPassword); + $oAccount->SetPassword($oNewPassword); if ($oAccount instanceof \RainLoop\Model\MainAccount) { $oActions->SetAuthToken($oAccount); + $oAccount->resealCryptKey($oPrevPassword); } return $this->jsonResponse(__FUNCTION__, $oActions->AppData(false)); } - public static function encrypt(string $algo, string $password) + public static function encrypt(string $algo, SensitiveString $password) { switch (\strtolower($algo)) { @@ -233,7 +245,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin private static function PasswordStrength(string $sPassword) : int { $i = \strlen($sPassword); - $max = min(100, $i * 8); + $max = \min(100, $i * 8); $s = 0; while (--$i) { $s += ($sPassword[$i] != $sPassword[$i-1] ? 1 : -0.5); diff --git a/plugins/change-password/langs/de_DE.ini b/plugins/change-password/langs/de.ini similarity index 90% rename from plugins/change-password/langs/de_DE.ini rename to plugins/change-password/langs/de.ini index 583cd5783..71360c000 100644 --- a/plugins/change-password/langs/de_DE.ini +++ b/plugins/change-password/langs/de.ini @@ -10,3 +10,4 @@ CURRENT_PASSWORD_INCORRECT = "Aktuelles Passwort falsch" CURRENT_PASSWORD_INCORRECT = "Aktuelles Passwort falsch" NEW_PASSWORD_SHORT = "Passwort ist zu kurz" NEW_PASSWORD_WEAK = "Passwort ist zu einfach" +NEW_PASSWORD_HIBP = "Passwort gefunden in Have I Been Pwned" diff --git a/plugins/change-password/langs/en.ini b/plugins/change-password/langs/en.ini index 8d3db7374..4a4a6ea12 100644 --- a/plugins/change-password/langs/en.ini +++ b/plugins/change-password/langs/en.ini @@ -10,3 +10,4 @@ COULD_NOT_SAVE_NEW_PASSWORD = "Could not save new password" CURRENT_PASSWORD_INCORRECT = "Current password incorrect" NEW_PASSWORD_SHORT = "Password is too short" NEW_PASSWORD_WEAK = "Password is too easy" +NEW_PASSWORD_HIBP = "Password found in Have I Been Pwned" diff --git a/plugins/change-password/langs/en_GB.ini b/plugins/change-password/langs/en_GB.ini index 8d3db7374..4a4a6ea12 100644 --- a/plugins/change-password/langs/en_GB.ini +++ b/plugins/change-password/langs/en_GB.ini @@ -10,3 +10,4 @@ COULD_NOT_SAVE_NEW_PASSWORD = "Could not save new password" CURRENT_PASSWORD_INCORRECT = "Current password incorrect" NEW_PASSWORD_SHORT = "Password is too short" NEW_PASSWORD_WEAK = "Password is too easy" +NEW_PASSWORD_HIBP = "Password found in Have I Been Pwned" diff --git a/plugins/change-password/langs/en_US.ini b/plugins/change-password/langs/en_US.ini index 8d3db7374..4a4a6ea12 100644 --- a/plugins/change-password/langs/en_US.ini +++ b/plugins/change-password/langs/en_US.ini @@ -10,3 +10,4 @@ COULD_NOT_SAVE_NEW_PASSWORD = "Could not save new password" CURRENT_PASSWORD_INCORRECT = "Current password incorrect" NEW_PASSWORD_SHORT = "Password is too short" NEW_PASSWORD_WEAK = "Password is too easy" +NEW_PASSWORD_HIBP = "Password found in Have I Been Pwned" diff --git a/plugins/change-password/langs/es_ES.ini b/plugins/change-password/langs/es.ini similarity index 90% rename from plugins/change-password/langs/es_ES.ini rename to plugins/change-password/langs/es.ini index f545a2505..47bc3ecee 100644 --- a/plugins/change-password/langs/es_ES.ini +++ b/plugins/change-password/langs/es.ini @@ -10,3 +10,4 @@ COULD_NOT_SAVE_NEW_PASSWORD = "No se puede guardar la nueva contraseña" CURRENT_PASSWORD_INCORRECT = "La contraseña actual es incorrecta" NEW_PASSWORD_SHORT = "La contraseña es muy corta" NEW_PASSWORD_WEAK = "La contraseña es muy fácil" +NEW_PASSWORD_HIBP = "Contraseña encontrada en Have I Been Pwned" diff --git a/plugins/change-password/langs/fr_FR.ini b/plugins/change-password/langs/fr.ini similarity index 90% rename from plugins/change-password/langs/fr_FR.ini rename to plugins/change-password/langs/fr.ini index 60c449b2c..ecb563f83 100644 --- a/plugins/change-password/langs/fr_FR.ini +++ b/plugins/change-password/langs/fr.ini @@ -10,3 +10,4 @@ COULD_NOT_SAVE_NEW_PASSWORD = "Impossible d'enregistrer le nouveau mot de passe" CURRENT_PASSWORD_INCORRECT = "Le mot de passe actuel est incorrect" NEW_PASSWORD_SHORT = "Le mot de passe est trop court" NEW_PASSWORD_WEAK = "Le mot de passe n'est pas assez fort" +NEW_PASSWORD_HIBP = "Mot de passe trouvé dans Have I Been Pwned" diff --git a/plugins/change-password/langs/it_IT.ini b/plugins/change-password/langs/it.ini similarity index 91% rename from plugins/change-password/langs/it_IT.ini rename to plugins/change-password/langs/it.ini index e3495e465..62507ce53 100644 --- a/plugins/change-password/langs/it_IT.ini +++ b/plugins/change-password/langs/it.ini @@ -10,3 +10,4 @@ COULD_NOT_SAVE_NEW_PASSWORD = "Non è stato possibile salvare la nuova password" CURRENT_PASSWORD_INCORRECT = "La password attuale non è corretta" NEW_PASSWORD_SHORT = "La password scelta è troppo breve" NEW_PASSWORD_WEAK = "La password scelta non è abbastanza complessa" +NEW_PASSWORD_HIBP = "Password trovata in Have I Been Pwned" diff --git a/plugins/change-password/langs/nl_NL.ini b/plugins/change-password/langs/nl.ini similarity index 90% rename from plugins/change-password/langs/nl_NL.ini rename to plugins/change-password/langs/nl.ini index 9eff931ad..9b6078e1d 100644 --- a/plugins/change-password/langs/nl_NL.ini +++ b/plugins/change-password/langs/nl.ini @@ -10,3 +10,4 @@ COULD_NOT_SAVE_NEW_PASSWORD = "Nieuwe wachtwoord kon niet opgeslagen worden" CURRENT_PASSWORD_INCORRECT = "Huidig wachtwoord onjuist" NEW_PASSWORD_SHORT = "Wachtwoord is te kort" NEW_PASSWORD_WEAK = "Wachtwoord is te makkelijk" +NEW_PASSWORD_HIBP = "Wachtwoord gevonden in Have I Been Pwned" diff --git a/plugins/change-password/langs/zh_CN.ini b/plugins/change-password/langs/zh.ini similarity index 89% rename from plugins/change-password/langs/zh_CN.ini rename to plugins/change-password/langs/zh.ini index 9aab9f805..9f902096b 100644 --- a/plugins/change-password/langs/zh_CN.ini +++ b/plugins/change-password/langs/zh.ini @@ -10,3 +10,4 @@ COULD_NOT_SAVE_NEW_PASSWORD = "无法保存新密码" CURRENT_PASSWORD_INCORRECT = "当前密码不正确" NEW_PASSWORD_SHORT = "密码太短" NEW_PASSWORD_WEAK = "密码过于简单" +NEW_PASSWORD_HIBP = "在 Have I Been Pwned 中找到密码" diff --git a/plugins/compact-composer/css/composer.css b/plugins/compact-composer/css/composer.css new file mode 100644 index 000000000..8e0bfff15 --- /dev/null +++ b/plugins/compact-composer/css/composer.css @@ -0,0 +1,92 @@ +.CompactComposer .squire-toolbar { + padding-top: 4px; + padding-bottom: 0; + overflow: visible; + z-index: 200; + white-space: normal; + min-height: auto; +} + +.CompactComposer .squire-toolbar > .btn-group { + margin-bottom: 4px; +} + +.CompactComposer .squire-toolbar > .btn-group > a.btn, +.CompactComposer .squire-toolbar button.btn, +.CompactComposer .squire-toolbar select.btn { + line-height: 20px; + padding-top: 4px; + padding-bottom: 4px; + min-height: 24px; +} + +.squire-toolbar-menu-item { + display: flex; + align-items: center; + gap: .25em; + margin: .1em !important; + cursor: pointer; + padding: .25em; +} + +.squire-toolbar-menu-item.active { + background-color: rgba(128, 128, 128, .1); +} + +.squire-toolbar-menu-item:hover { + background-color: rgba(128, 128, 128, .2); +} + +.squire-toolbar-svg-icon { + display: block; + fill: var(--dialog-clr, #333); +} +.squire-toolbar-menu .squire-toolbar-svg-icon { + display: block; + fill: var(--dropdown-menu-color, inherit); +} + +.squire2-mode-wysiwyg .squire-plain, +.squire2-mode-source .squire-wysiwyg, +.squire2-mode-plain .squire-wysiwyg { + display: none; +} + +.squire2-mode-source .squire-plain, +.squire2-mode-plain .squire-plain { + display: block; +} + +.CompactComposer .squire-toolbar > .squire-toolbar-menu-wrap:last-child { + float: right; +} + +.CompactComposer .squire-toolbar.mode-plain .squire-html-mode-item { + display: none; +} + +#V-PopupsCompose .attachmentAreaParent.compact { + height: auto; + min-height: auto; + padding: 0; + overflow: auto; + flex: 1 0 auto; + max-height: 12em; + margin: 0; +} + +#V-PopupsCompose .compact > .b-attachment-place { + position: static; + display: none; + margin: .375em; + line-height: 4em; +} + +#V-PopupsCompose .compact > .b-attachment-place.dragAndDropOver { + display: block; +} + +#V-PopupsCompose .compact .attachmentList { + margin: 0; + padding: 0; +} diff --git a/plugins/compact-composer/index.php b/plugins/compact-composer/index.php new file mode 100644 index 000000000..ae743ca79 --- /dev/null +++ b/plugins/compact-composer/index.php @@ -0,0 +1,22 @@ +addCss('css/composer.css'); + $this->addJs('js/squire-raw.js'); + $this->addJs('js/parsel.js'); + $this->addJs('js/CompactComposer.js'); + } +} diff --git a/plugins/compact-composer/js/CompactComposer.js b/plugins/compact-composer/js/CompactComposer.js new file mode 100644 index 000000000..288fb497c --- /dev/null +++ b/plugins/compact-composer/js/CompactComposer.js @@ -0,0 +1,1008 @@ +/* eslint max-len: 0 */ +(win => { + + const rl = win.rl; + + if (!rl) { + return; + } + + rl.registerWYSIWYG('CompactComposer', (owner, container, onReady) => { + const editor = new CompactComposer(container); + onReady(editor); + }); + + const doc = win.document; + + addEventListener('rl-view-model', e => { + const vm = e.detail; + if ('PopupsCompose' === vm.viewModelTemplateID && rl.settings.get('editorWysiwyg') === 'CompactComposer') { + vm.querySelector('.tabs label[for="tab-body"]').dataset.bind = "visible: canMailvelope"; + // Now move the attachments tab to the bottom of the screen + const + input = vm.querySelector('.tabs input[value="attachments"]'), + label = vm.querySelector('.tabs label[for="tab-attachments"]'), + area = vm.querySelector('.tabs .attachmentAreaParent'); + input.remove(); + label.remove(); + area.remove(); + area.classList.add('compact'); + area.querySelector('.b-attachment-place').dataset.bind = "visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}"; + vm.viewModelDom.append(area); + // There is a better way to do this probably, + // but we need this for drag and drop to work + e.detail.attachmentsArea = e.detail.bodyArea; + } + }); + + const + removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE', + allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,EM,FONT,H1,H2,H3,H4,H5,H6,HR,I,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL', + allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','), + + // TODO: labels translations + i18n = (str, def) => rl.i18n(str) || def, + + ctrlKey = shortcuts.getMetaKey() + ' + ', + + createElement = name => doc.createElement(name), + + tpl = createElement('template'), + + trimLines = html => html.trim().replace(/^(
\s*\s*<\/div>)+/, '').trim(), + htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(), + plainToHtml = text => rl.Utils.plainToHtml(text), + + getFragmentOfChildren = parent => { + let frag = doc.createDocumentFragment(); + frag.append(...parent.childNodes); + return frag; + }, + + /** + * @param {Array} data + * @param {String} prop + */ + getByProp = (data, prop) => { + for (let i = 0; i < data.length; i++) { + const outer = data[i]; + if (outer.hasOwnProperty(prop)) { + return outer; + } + if (outer.items && Array.isArray(outer.items)) { + const item = outer.items.find(item => item.prop === prop); + if (item) { + return item; + } + } + } + throw new Error('item with prop ' + prop + ' not found'); + }, + + SquireDefaultConfig = { + /* + addLinks: true // allow_smart_html_links + */ + sanitizeToDOMFragment: (html) => { + tpl.innerHTML = (html || '') + .replace(/<\/?(BODY|HTML)[^>]*>/gi, '') + .replace(//g, '') + .replace(/]*>\s*<\/span>/gi, '') + .trim(); + tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove()); + return tpl.content; + } + }, + + pasteSanitizer = (event) => { + const frag = event.detail.fragment; + frag.querySelectorAll('a:empty,span:empty').forEach(el => el.remove()); + frag.querySelectorAll(removeElements).forEach(el => el.remove()); + frag.querySelectorAll('*').forEach(el => { + if (!el.matches(allowedElements)) { + el.replaceWith(getFragmentOfChildren(el)); + } else if (el.hasAttributes()) { + [...el.attributes].forEach(attr => { + let name = attr.name.toLowerCase(); + if (!allowedAttributes.includes(name)) { + el.removeAttribute(name); + } + }); + } + }); + }, + + pasteImageHandler = (e, squire) => { + + const items = [...e.detail.clipboardData.items]; + const imageItems = items.filter((item) => /image/.test(item.type)); + if (!imageItems.length) { + return false; + } + let reader = new FileReader(); + reader.onload = (loadEvent) => { + squire.insertImage(loadEvent.target.result); + }; + reader.readAsDataURL(imageItems[0].getAsFile()); + }; + + + class CompactComposer { + constructor(container) { + const + plain = createElement('textarea'), + wysiwyg = createElement('div'), + toolbar = createElement('div'), + squire = new win.Squire2(wysiwyg, SquireDefaultConfig); + + this.container = container; + container.classList.add('CompactComposer'); + + plain.className = 'squire-plain'; + wysiwyg.className = 'squire-wysiwyg'; + wysiwyg.dir = 'auto'; + this.mode = ''; // 'plain' | 'wysiwyg' + this.squire = squire; + this.plain = plain; + this.wysiwyg = wysiwyg; + this.toolbar = toolbar; + + toolbar.className = 'squire-toolbar btn-toolbar'; + const actions = this.makeActions(squire, toolbar); + + this.squire.addEventListener('willPaste', pasteSanitizer); + this.squire.addEventListener('pasteImage', (e) => { + pasteImageHandler(e, squire); + }); + + wysiwyg.addEventListener('focus', () => { + const range = this.squire.getSelection(); + if (range.collapsed && range.startContainer === wysiwyg) { + // when the caret is directly in the wysiwyg a bunch of stuff + // (like lists, blockquotes, etc...) do not work, + // so we need to place it inside the nearest element + if (wysiwyg.children[range.startOffset] !== undefined) { + const newRange = document.createRange(); + newRange.setStart(wysiwyg.children[range.startOffset], 0); + this.squire.setSelection(newRange); + } + } + }); + +// squire.addEventListener('focus', () => shortcuts.off()); +// squire.addEventListener('blur', () => shortcuts.on()); + + container.append(toolbar, wysiwyg, plain); + + const fontFamilySelect = getByProp(actions, 'fontFamily').element; + + const fontSizeAction = getByProp(actions, 'fontSize'); + + /** + * @param {string} fontName + * @return {string} + */ + const normalizeFontName = (fontName) => fontName.trim().replace(/(^["']*|["']*$)/g, '').trim().toLowerCase(); + + /** @type {string[]} - lower cased array of available font families*/ + const fontFamiliesLowerCase = Object.values(fontFamilySelect.options).map(option => option.value.toLowerCase()); + + /** + * A theme might have CSS like div.squire-wysiwyg[contenteditable="true"] { + * font-family: 'Times New Roman', Times, serif; } + * so let's find the best match squire.getRoot()'s font + * it will also help to properly handle generic font names like 'sans-serif' + * @type {number} + */ + let defaultFontFamilyIndex = 0; + const squireRootFonts = getComputedStyle(squire.getRoot()).fontFamily.split(',').map(normalizeFontName); + fontFamiliesLowerCase.some((family, index) => { + const matchFound = family.split(',').some(availableFontName => { + const normalizedFontName = normalizeFontName(availableFontName); + return squireRootFonts.some(squireFontName => squireFontName === normalizedFontName); + }); + if (matchFound) { + defaultFontFamilyIndex = index; + } + return matchFound; + }); + + /** + * Instead of comparing whole 'font-family' strings, + * we are going to look for individual font names, because we might be + * editing a Draft started in another email client for example + * + * @type {Object.} + */ + const fontNamesMap = {}; + /** + * @param {string} fontFamily + * @param {number} index + */ + const processFontFamilyString = (fontFamily, index) => { + fontFamily.split(',').forEach(fontName => { + const key = normalizeFontName(fontName); + if (fontNamesMap[key] === undefined) { + fontNamesMap[key] = index; + } + }); + }; + // first deal with the default font family + processFontFamilyString(fontFamiliesLowerCase[defaultFontFamilyIndex], defaultFontFamilyIndex); + // and now with the rest of the font families + fontFamiliesLowerCase.forEach((fontFamily, index) => { + if (index !== defaultFontFamilyIndex) { + processFontFamilyString(fontFamily, index); + } + }); + + // ----- + + let ignoreNextSelectEvent = false; + + squire.addEventListener('pathChange', e => { + + const tokensMap = this.buildTokensMap(e.detail); + + if (tokensMap.has('__selection__')) { + ignoreNextSelectEvent = false; + return; + } + this.indicators.forEach((indicator) => { + indicator.element.classList.toggle('active', indicator.selectors.some(selector => tokensMap.has(selector))); + }); + + let familySelectedIndex = defaultFontFamilyIndex; + const fontFamily = tokensMap.get('__font_family__'); + if (fontFamily) { + familySelectedIndex = -1; // show empty select if we don't know the font + const fontNames = fontFamily.split(','); + for (let i = 0; i < fontNames.length; i++) { + const index = fontNamesMap[normalizeFontName(fontNames[i])]; + if (index !== undefined) { + familySelectedIndex = index; + break; + } + } + } + fontFamilySelect.selectedIndex = familySelectedIndex; + + let sizeSelectedIndex = fontSizeAction.defaultValueIndex; + const fontSize = tokensMap.get('__font_size__'); + if (fontSize) { + // -1 is ok because it will just show a blank +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ +
+ + +
\ No newline at end of file diff --git a/plugins/search-filters/templates/PopupsSearchFilters.html b/plugins/search-filters/templates/PopupsSearchFilters.html new file mode 100644 index 000000000..c85ca8914 --- /dev/null +++ b/plugins/search-filters/templates/PopupsSearchFilters.html @@ -0,0 +1,54 @@ +
+ × +

+
+ +
+ +
diff --git a/plugins/search-filters/templates/STabSearchFilters.html b/plugins/search-filters/templates/STabSearchFilters.html new file mode 100644 index 000000000..d006da345 --- /dev/null +++ b/plugins/search-filters/templates/STabSearchFilters.html @@ -0,0 +1,39 @@ +
+
+
+ +     + +
+ +
+ +
+ + + +
+ + +
+
+ + +
+
\ No newline at end of file diff --git a/plugins/smtp-use-from-adr-account/README b/plugins/smtp-use-from-adr-account/README new file mode 100644 index 000000000..e4a141007 --- /dev/null +++ b/plugins/smtp-use-from-adr-account/README @@ -0,0 +1,5 @@ +What does it do? + +You can configure multible identities, but if you send eMails it depends on the smtp server whether it accepts sending mails for an foreign eMail-Adress. +By default, the smtp server of the account currently displayed in the interface is used. +The plugin checks if you use a different From-Adress (identity). Then it searchs for a matching account (for the user) and rewrites smpt-config and credentials. diff --git a/plugins/smtp-use-from-adr-account/index.php b/plugins/smtp-use-from-adr-account/index.php new file mode 100644 index 000000000..cea9eab3b --- /dev/null +++ b/plugins/smtp-use-from-adr-account/index.php @@ -0,0 +1,115 @@ +addHook('filter.smtp-from', 'FilterDetectFrom'); + $this->addHook('smtp.before-connect', 'FilterSmtpConnect'); + $this->addHook('smtp.before-login', 'FilterSmtpCredentials'); + } + + /** + * \RainLoop\Model\Account $oAccount + * \MailSo\Mime\Message $oMessage + * string &$sFrom + */ + public function FilterDetectFrom(\RainLoop\Model\Account $oAccount, \MailSo\Mime\Message $oMessage, string &$sFrom) + { + $sWhiteList = \trim($this->Config()->Get('plugin', 'from_adress_pattern', '')); + $sFoundValue = ''; + if (\strlen($sWhiteList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sFrom, $sWhiteList, $sFoundValue) && $sFrom != $oAccount->Email()) { + \SnappyMail\LOG::info(get_class($this) ,'From address different from account recognized: '. $oAccount->Email().' -> '.$sFrom . '(~ '.$sFoundValue.')'); + $oMainAccount; + $oFromAccount; + if ($oAccount instanceof \RainLoop\Model\MainAccount ) { + $oMainAccount=$oAccount; + } else { + $oMainAccount=$this->Manager()->Actions()->getMainAccountFromToken(); + if ($oMainAccount->Email() == $sFrom) { + $this->aFromAccount[$oAccount->Email()]=$oMainAccount; + return; + } + } + $aAccounts = $this->Manager()->Actions()->getAccounts($oMainAccount); + foreach ($aAccounts as &$value) { + $oValue=\RainLoop\Model\AdditionalAccount::NewInstanceFromTokenArray($this->Manager()->Actions(), $value); + if ($oValue->Email()==$sFrom) { + $oFromAccount = $oValue; + break; + } + } + if (is_null($oFromAccount)){ + \SnappyMail\LOG::info(get_class($this),'No Account found for '. $sFrom); + if ($this->Config()->Get('plugin', 'throw_notfound_exception', true)) { + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountDoesNotExist); + } + return; + } + $this->aFromAccount[$oAccount->Email()]=$oFromAccount; + } + } + /** + * @param \RainLoop\Model\Account $oAccount + * @param \MailSo\Smtp\SmtpClient $oSmtpClient + * @param \MailSo\Smtp\Settings $oSettings + */ + public function FilterSmtpConnect(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, \MailSo\Smtp\Settings $oSettings) + { + if ( isset($this->aFromAccount[$oAccount->Email()]) ) { + $oFromAccount = $this->aFromAccount[$oAccount->Email()]; + $oSettings->host = $oFromAccount->Domain()->SmtpSettings()->host; + $oSettings->port = (int) $oFromAccount->Domain()->SmtpSettings()->port; + $oSettings->type = $oFromAccount->Domain()->SmtpSettings()->type; + \SnappyMail\LOG::info(get_class($this),'Smtp config rewrite: '. $oSettings->host); + } + } + + /** + * @param \RainLoop\Model\Account $oAccount + * @param \MailSo\Smtp\SmtpClient $oSmtpClient + * @param \MailSo\Smtp\Settings $oSettings + */ + public function FilterSmtpCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, \MailSo\Smtp\Settings $oSettings) + { + if ( isset($this->aFromAccount[$oAccount->Email()]) ) { + $oFromAccount = $this->aFromAccount[$oAccount->Email()]; + unset($this->aFromAccount[$oAccount->Email()]); + $oSettings->useAuth = $oFromAccount->Domain()->SmtpSettings()->useAuth; + $oSettings->username = $oFromAccount->OutLogin(); + $oSettings->passphrase = $oFromAccount->IncPassword(); + \SnappyMail\LOG::info(get_class($this),'user/pwd rewrite: '. $oFromAccount->Email()); + } + } + + /** + * @return array + */ + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('from_adress_pattern')->SetLabel('From-Address pattern') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) + ->SetDescription('space as delimiter, wildcard supported.') + ->SetDefaultValue('user@example.com *@example2.com'), + \RainLoop\Plugins\Property::NewInstance('throw_notfound_exception')->SetLabel('Throw Exception, if from-adr is not found as account') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDescription('it is not possible to send eMails in this case, regardless of whether the smtp-server would do it') + ->SetDefaultValue(true) + ); + } + +} diff --git a/plugins/two-factor-auth/index.php b/plugins/two-factor-auth/index.php index 61f583e9f..a080ae08b 100644 --- a/plugins/two-factor-auth/index.php +++ b/plugins/two-factor-auth/index.php @@ -1,15 +1,16 @@ removeBackupCodeFromTwoFactorInfo($oAccount->Email(), $sCode); + $this->removeBackupCodeFromTwoFactorInfo($oAccount, $sCode); } } @@ -110,11 +111,7 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin $sSecret = $this->TwoFactorAuthProvider($oAccount)->CreateSecret(); - $aCodes = array(); - for ($iIndex = 9; $iIndex > 0; $iIndex--) - { - $aCodes[] = \rand(100000000, 900000000); - } + $aCodes = \array_map(function(){return \rand(100000000, 900000000);}, \array_fill(0, 8, null)); $this->StorageProvider()->Put($oAccount, \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, @@ -123,7 +120,7 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin 'User' => $sEmail, 'Enable' => false, 'Secret' => $sSecret, - 'QRCode' => static::getQRCode($sEmail, $sSecret), + 'QRCode' => static::getQRCode($oAccount, $sSecret), 'BackupCodes' => \implode(' ', $aCodes) )) ); @@ -131,9 +128,9 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin return $this->jsonResponse(__FUNCTION__, $this->getTwoFactorInfo($oAccount)); } - private static function getQRCode(string $email, string $secret) : string + private static function getQRCode(MainAccount $oAccount, string $secret) : string { - $email = \rawurlencode($email); + $email = \rawurlencode($oAccount->Email()); // $issuer = \rawurlencode(\RainLoop\API::Config()->Get('webmail', 'title', 'SnappyMail')); $QR = \SnappyMail\QRCode::getMinimumQRCode( // "otpauth://totp/{$issuer}:{$email}?secret={$secret}&issuer={$issuer}", @@ -154,7 +151,7 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin $aResult = $this->getTwoFactorInfo($oAccount); unset($aResult['BackupCodes']); - $aResult['QRCode'] = static::getQRCode($oAccount->Email(), $aResult['Secret']); + $aResult['QRCode'] = static::getQRCode($oAccount, $aResult['Secret']); return $this->jsonResponse(__FUNCTION__, $aResult); } @@ -177,8 +174,7 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin $bResult = false; $mData = $this->getTwoFactorInfo($oAccount); - if (isset($mData['Secret'], $mData['BackupCodes'])) - { + if (isset($mData['Secret'], $mData['BackupCodes'])) { $bResult = $this->StorageProvider()->Put($oAccount, \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, 'two_factor', @@ -240,7 +236,7 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin return $this->Manager()->Actions()->StorageProvider(); } - private $oTwoFactorAuthProvider; + private $oTwoFactorAuthProvider = null; protected function TwoFactorAuthProvider(MainAccount $oAccount) : ?TwoFactorAuthInterface { if (!$this->oTwoFactorAuthProvider) { @@ -265,8 +261,7 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin 'BackupCodes' => '' ); - if (!empty($sEmail)) - { + if (!empty($sEmail)) { $aResult['User'] = $sEmail; $sData = $this->StorageProvider()->Get($oAccount, @@ -274,8 +269,7 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin 'two_factor' ); - if ($sData) - { + if ($sData) { $mData = static::DecodeKeyValues($sData); } } @@ -288,18 +282,15 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin $aResult['Enable'] = isset($mData['Enable']) ? !!$mData['Enable'] : false; $aResult['Secret'] = $mData['Secret']; $aResult['BackupCodes'] = $mData['BackupCodes']; - $aResult['QRCode'] = static::getQRCode($oAccount->Email(), $mData['Secret']); + $aResult['QRCode'] = static::getQRCode($oAccount, $mData['Secret']); } - if ($bRemoveSecret) - { - if (isset($aResult['Secret'])) - { + if ($bRemoveSecret) { + if (isset($aResult['Secret'])) { unset($aResult['Secret']); } - if (isset($aResult['BackupCodes'])) - { + if (isset($aResult['BackupCodes'])) { unset($aResult['BackupCodes']); } } @@ -309,8 +300,7 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin protected function removeBackupCodeFromTwoFactorInfo(MainAccount $oAccount, string $sCode) : bool { - if (!$oAccount || empty($sCode)) - { + if (!$oAccount || empty($sCode)) { return false; } @@ -319,12 +309,10 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin 'two_factor' ); - if ($sData) - { + if ($sData) { $mData = static::DecodeKeyValues($sData); - if (!empty($mData['BackupCodes'])) - { + if (!empty($mData['BackupCodes'])) { $sBackupCodes = \preg_replace('/[^\d]+/', ' ', ' '.$mData['BackupCodes'].' '); $sBackupCodes = \str_replace(' '.$sCode.' ', '', $sBackupCodes); diff --git a/plugins/two-factor-auth/js/TwoFactorAuthLogin.js b/plugins/two-factor-auth/js/TwoFactorAuthLogin.js index d4e4c61e5..aa71747c2 100644 --- a/plugins/two-factor-auth/js/TwoFactorAuthLogin.js +++ b/plugins/two-factor-auth/js/TwoFactorAuthLogin.js @@ -17,7 +17,7 @@ + '' + '' + '')); @@ -27,7 +27,7 @@ // https://github.com/the-djmaze/snappymail/issues/349 addEventListener('sm-show-screen', e => { - if ('settings' !== e.detail && rl.settings.get('SetupTwoFactor')) { + if (!e.detail.startsWith('settings') && rl.settings.get('SetupTwoFactor')) { e.preventDefault(); forceTOTP(); } diff --git a/plugins/two-factor-auth/langs/cs-CZ.ini b/plugins/two-factor-auth/langs/cs.ini similarity index 100% rename from plugins/two-factor-auth/langs/cs-CZ.ini rename to plugins/two-factor-auth/langs/cs.ini diff --git a/plugins/two-factor-auth/langs/de_DE.ini b/plugins/two-factor-auth/langs/de.ini similarity index 100% rename from plugins/two-factor-auth/langs/de_DE.ini rename to plugins/two-factor-auth/langs/de.ini diff --git a/plugins/two-factor-auth/langs/es_ES.ini b/plugins/two-factor-auth/langs/es.ini similarity index 100% rename from plugins/two-factor-auth/langs/es_ES.ini rename to plugins/two-factor-auth/langs/es.ini diff --git a/plugins/two-factor-auth/langs/fr_FR.ini b/plugins/two-factor-auth/langs/fr.ini similarity index 100% rename from plugins/two-factor-auth/langs/fr_FR.ini rename to plugins/two-factor-auth/langs/fr.ini diff --git a/plugins/two-factor-auth/langs/hu-HU.ini b/plugins/two-factor-auth/langs/hu.ini similarity index 100% rename from plugins/two-factor-auth/langs/hu-HU.ini rename to plugins/two-factor-auth/langs/hu.ini diff --git a/plugins/two-factor-auth/langs/it_IT.ini b/plugins/two-factor-auth/langs/it.ini similarity index 100% rename from plugins/two-factor-auth/langs/it_IT.ini rename to plugins/two-factor-auth/langs/it.ini diff --git a/plugins/two-factor-auth/langs/nl_NL.ini b/plugins/two-factor-auth/langs/nl.ini similarity index 100% rename from plugins/two-factor-auth/langs/nl_NL.ini rename to plugins/two-factor-auth/langs/nl.ini diff --git a/plugins/two-factor-auth/langs/pl_PL.ini b/plugins/two-factor-auth/langs/pl.ini similarity index 100% rename from plugins/two-factor-auth/langs/pl_PL.ini rename to plugins/two-factor-auth/langs/pl.ini diff --git a/plugins/two-factor-auth/langs/sv-SE.ini b/plugins/two-factor-auth/langs/sv.ini similarity index 100% rename from plugins/two-factor-auth/langs/sv-SE.ini rename to plugins/two-factor-auth/langs/sv.ini diff --git a/plugins/two-factor-auth/langs/zh_CN.ini b/plugins/two-factor-auth/langs/zh.ini similarity index 100% rename from plugins/two-factor-auth/langs/zh_CN.ini rename to plugins/two-factor-auth/langs/zh.ini diff --git a/plugins/video-on-login-screen/LICENSE b/plugins/video-on-login-screen/LICENSE new file mode 100755 index 000000000..44b915a0c --- /dev/null +++ b/plugins/video-on-login-screen/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2023 SnappyMail Team + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/video-on-login-screen/README b/plugins/video-on-login-screen/README new file mode 100755 index 000000000..ec8c5bbdb --- /dev/null +++ b/plugins/video-on-login-screen/README @@ -0,0 +1 @@ +Fullscreen background video on login screen. \ No newline at end of file diff --git a/plugins/video-on-login-screen/VERSION b/plugins/video-on-login-screen/VERSION new file mode 100755 index 000000000..b123147e2 --- /dev/null +++ b/plugins/video-on-login-screen/VERSION @@ -0,0 +1 @@ +1.1 \ No newline at end of file diff --git a/plugins/video-on-login-screen/index.php b/plugins/video-on-login-screen/index.php new file mode 100755 index 000000000..26f29b953 --- /dev/null +++ b/plugins/video-on-login-screen/index.php @@ -0,0 +1,44 @@ +addJs('js/video-on-login.js'); + $this->addHook('main.content-security-policy', 'ContentSecurityPolicy'); + } + + /** + * @return array + */ + protected function configMapping() : array + { + return array( + \RainLoop\Plugins\Property::NewInstance('mp4_file')->SetLabel('Url to a mp4 file') + ->SetPlaceholder('http://') + ->SetAllowedInJs(true) + ->SetDefaultValue(''), + \RainLoop\Plugins\Property::NewInstance('playback_rate')->SetLabel('Playback rate') + ->SetAllowedInJs(true) + ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) + ->SetDefaultValue(array('100%', '25%', '50%', '75%', '125%', '150%', '200%')), + ); + } + + public function ContentSecurityPolicy(\SnappyMail\HTTP\CSP $CSP) + { + $vSource = $this->Config()->Get('plugin', 'mp4_file', 'self'); + $CSP->add('media-src', $vSource); + } +} diff --git a/plugins/video-on-login-screen/js/video-on-login.js b/plugins/video-on-login-screen/js/video-on-login.js new file mode 100755 index 000000000..d5d497a62 --- /dev/null +++ b/plugins/video-on-login-screen/js/video-on-login.js @@ -0,0 +1,107 @@ +(rl => { + + rl && addEventListener('rl-view-model', e => { + const id = e.detail.viewModelTemplateID; + if (e.detail && ('AdminLogin' === id || 'Login' === id)) { + let + nId = null, + script; + + let + iRate = 1, + sRate = window.rl.pluginSettingsGet('video-on-login-screen', 'playback_rate') + ; + + switch (sRate) + { + case '25%': + iRate = 0.25; + break; + case '50%': + iRate = 0.5; + break; + case '75%': + iRate = 0.75; + break; + case '125%': + iRate = 1.25; + break; + case '150%': + iRate = 1.5; + break; + case '200%': + iRate = 2; + break; + } + const + mode = 'Login' === id ? 'user' : 'admin', + + doc = document, + loginContainer = doc.querySelectorAll('#V-Login #V-AdminLogin'), + container = doc.querySelector('#rl-content'), + + ShowVideo = () => { + if (loginContainer) { + var stEl = doc.createElement('style'); + stEl.innerHTML = + ` + #video-el { + z-index: -1; + overflow: hidden; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; + height: 100vh; + width: 100%; + object-fit: cover; + } + ` + var ref = doc.querySelector('script'); + ref.parentNode.insertBefore(stEl, ref); + + const oEl = doc.createElement('div'); + oEl.className = 'video-div'; + const vEl = doc.createElement('video'); + vEl.setAttribute('loop', true); + vEl.setAttribute('playsinline', ''); + vEl.setAttribute('muted', ''); + vEl.setAttribute('autoplay', ''); + vEl.muted = true; + vEl.setAttribute('playbackRate', iRate); + vEl.setAttribute('id', 'video-el'); + oEl.appendChild(vEl); + const sEl = doc.createElement('source'); + sEl.setAttribute('src', rl.pluginSettingsGet('video-on-login-screen', 'mp4_file')); + sEl.setAttribute('type', 'video/mp4'); + vEl.appendChild(sEl); + + container.before(oEl); + + } + }, + + DestroyVideo = () => { + const vEl = doc.querySelector('#video-el'); + if (vEl) { + vEl.parentElement.removeChild(vEl); + } + }; + + window.ShowVideo = ShowVideo; + + window.DestroyVideo = DestroyVideo; + + ShowVideo(); + + addEventListener(`sm-${mode}-login-response`, e => { + if (!e.detail.error) { + DestroyVideo(); + } + }); + } + }); + +})(window.rl); diff --git a/plugins/view-ics/index.php b/plugins/view-ics/index.php index febaeb875..ca95c0876 100644 --- a/plugins/view-ics/index.php +++ b/plugins/view-ics/index.php @@ -4,15 +4,16 @@ class ViewICSPlugin extends \RainLoop\Plugins\AbstractPlugin { const NAME = 'View ICS', - VERSION = '2.0', - RELEASE = '2023-08-28', + VERSION = '2.2', + RELEASE = '2024-06-29', CATEGORY = 'Messages', - DESCRIPTION = 'Display ICS attachment details', - REQUIRED = '2.27.0'; + DESCRIPTION = 'Display ICS attachment or JSON-LD details', + REQUIRED = '2.34.0'; public function Init() : void { // $this->UseLangs(true); $this->addJs('message.js'); + $this->addJs('windowsZones.js'); } } diff --git a/plugins/view-ics/message.js b/plugins/view-ics/message.js index cb768402e..38153361f 100644 --- a/plugins/view-ics/message.js +++ b/plugins/view-ics/message.js @@ -8,7 +8,29 @@ template = document.getElementById(templateId), view = e.detail, attachmentsPlace = template.content.querySelector('.attachmentsPlace'), - dateRegEx = /(TZID=(?[^:]+):)?(?[0-9]{4})(?[0-9]{2})(?[0-9]{2})T(?[0-9]{2})(?[0-9]{2})(?[0-9]{2})(?Z?)/; + dateRegEx = /(TZID=(?[^:]+):)?(?[0-9]{4})(?[0-9]{2})(?[0-9]{2})T(?[0-9]{2})(?[0-9]{2})(?[0-9]{2})(?Z?)/, + parseDate = str => { + let parts = dateRegEx.exec(str)?.groups, + options = {dateStyle: 'long', timeStyle: 'short'}, + date = (parts ? new Date( + parseInt(parts.year, 10), + parseInt(parts.month, 10) - 1, + parseInt(parts.day, 10), + parseInt(parts.hour, 10), + parseInt(parts.minute, 10), + parseInt(parts.second, 10) + ) : new Date(str)); + parts?.tz && (options.timeZone = windowsVTIMEZONEs[parts.tz] || parts.tz); + try { + return date.format(options); + } catch (e) { + console.error(e); + if (options.timeZone) { + options.timeZone = undefined; + return date.format(options); + } + } + }; attachmentsPlace.after(Element.fromHTML(`
@@ -26,12 +48,45 @@ view.viewICS = ko.observable(null); + view.saveICS = () => { + let VEVENT = view.VEVENT(); + if (VEVENT) { + if (rl.nextcloud && VEVENT.rawText) { + rl.nextcloud.selectCalendar() + .then(href => href && rl.nextcloud.calendarPut(href, VEVENT)); + } else { + // TODO + } + } + } + /** * TODO */ view.message.subscribe(msg => { view.viewICS(null); if (msg) { + // JSON-LD after parsing HTML + // See http://schema.org/ + msg.linkedData.subscribe(data => { + if (!view.viewICS()) { + data.forEach(item => { + if (item["ical:summary"]) { + let VEVENT = { + SUMMARY: item["ical:summary"], + DTSTART: parseDate(item["ical:dtstart"]), +// DTEND: parseDate(item["ical:dtend"]), +// TRANSP: item["ical:transp"], +// LOCATION: item["ical:location"], + ATTENDEE: [] + } + view.viewICS(VEVENT); + return; + } + }); + } + }); + // ICS attachment // let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType); let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType); if (ics && ics.download) { @@ -71,17 +126,7 @@ VEVENT[line[1]].push(line[2]); } else { if ('DTSTART' === line[1] || 'DTEND' === line[1]) { - let parts = dateRegEx.exec(line[2])?.groups, - options = {dateStyle: 'long', timeStyle: 'short'}; - parts.tz && (options.timeZone = parts.tz); - line[2] = new Date( - parseInt(parts.year, 10), - parseInt(parts.month, 10) - 1, - parseInt(parts.day, 10), - parseInt(parts.hour, 10), - parseInt(parts.minute, 10), - parseInt(parts.second, 10) - ).format(options); + line[2] = parseDate(line[2]); } VEVENT[line[1]] = line[2]; } diff --git a/plugins/view-ics/style.css b/plugins/view-ics/style.css new file mode 100644 index 000000000..b3a4e5c8c --- /dev/null +++ b/plugins/view-ics/style.css @@ -0,0 +1,44 @@ + +/** + * .SML-@type where @type is the value of the JSON-LD "@type": + * See http://schema.org/ + */ + +.SML-FlightReservation { +} + + .SML-Airline { + } + + .SML-Airport { + } + + .SML-Flight { + } + +.SML-FoodEstablishmentReservation { +} + + .SML-FoodEstablishment { + } + +.SML-ParcelDelivery { +} + + .SML-Order { + } + + .SML-Organization { + } + + .SML-Product { + } + + .SML-PostalAddress { + } + +.SML-Person { +} + +.SML-PromotionCard { +} diff --git a/plugins/view-ics/windowsZones.js b/plugins/view-ics/windowsZones.js new file mode 100644 index 000000000..1c2c915c3 --- /dev/null +++ b/plugins/view-ics/windowsZones.js @@ -0,0 +1,738 @@ +// Windows timezones (Subset from https://github.com/unicode-cldr/cldr-core/blob/master/supplemental/windowsZones.json) +const windowsVTIMEZONEs = { + "_unicodeVersion": "13.0.0", + "_cldrVersion": "37", + // Windows : [IANA...] + "Afghanistan Standard Time": [ + "Asia/Kabul" + ], + "Alaskan Standard Time": [ + "America/Anchorage"/*, + "America/Juneau", + "America/Metlakatla", + "America/Nome", + "America/Sitka", + "America/Yakutat"*/ + ], + "Aleutian Standard Time": [ + "America/Adak" + ], + "Altai Standard Time": [ + "Asia/Barnaul" + ], + "Arab Standard Time": [ + "Asia/Aden"/*, + "Asia/Bahrain", + "Asia/Kuwait", + "Asia/Qatar", + "Asia/Riyadh" + */], + "Arabian Standard Time": [ + "Asia/Dubai"/*, + "Asia/Muscat", + "Etc/GMT-4" + */], + "Arabic Standard Time": [ + "Asia/Baghdad" + ], + "Argentina Standard Time": [ + "America/Argentina/La_Rioja"/*, + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Buenos_Aires", + "America/Catamarca", + "America/Cordoba", + "America/Jujuy", + "America/Mendoza" + */], + "Astrakhan Standard Time": [ + "Europe/Astrakhan"/*, + "Europe/Ulyanovsk" + */], + "Atlantic Standard Time": [ + "America/Glace_Bay"/*, + "America/Goose_Bay", + "America/Halifax", + "America/Moncton", + "America/Thule", + "Atlantic/Bermuda" + */], + "AUS Central Standard Time": [ + "Australia/Darwin" + ], + "Aus Central W. Standard Time": [ + "Australia/Eucla" + ], + "AUS Eastern Standard Time": [ + "Australia/Melbourne"/*, + "Australia/Sydney" + */], + "Azerbaijan Standard Time": [ + "Asia/Baku" + ], + "Azores Standard Time": [ + "America/Scoresbysund"/*, + "Atlantic/Azores" + */], + "Bahia Standard Time": [ + "America/Bahia" + ], + "Bangladesh Standard Time": [ + "Asia/Dhaka"/*, + "Asia/Thimphu" + */], + "Belarus Standard Time": [ + "Europe/Minsk" + ], + "Bougainville Standard Time": [ + "Pacific/Bougainville" + ], + "Canada Central Standard Time": [ + "America/Regina"/*, + "America/Swift_Current" + */], + "Cape Verde Standard Time": [ + "Atlantic/Cape_Verde"/*, + "Etc/GMT+1" + */], + "Caucasus Standard Time": [ + "Asia/Yerevan" + ], + "Cen. Australia Standard Time": [ + "Australia/Adelaide"/*, + "Australia/Broken_Hill" + */], + "Central America Standard Time": [ + "America/Belize"/*, + "America/Costa_Rica", + "America/El_Salvador", + "America/Guatemala", + "America/Managua", + "America/Tegucigalpa", + "Pacific/Galapagos", + "Etc/GMT+6" + */], + "Central Asia Standard Time": [ + "Asia/Almaty"/*, + "Asia/Bishkek", + "Asia/Qostanay", + "Asia/Urumqi", + "Indian/Chagos", + "Antarctica/Vostok", + "Etc/GMT-6" + */], + "Central Brazilian Standard Time": [ + "America/Campo_Grande"/*, + "America/Cuiaba" + */], + "Central Europe Standard Time": [ + "Europe/Belgrade"/*, + "Europe/Bratislava", + "Europe/Budapest", + "Europe/Ljubljana", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Tirane" + */], + "Central European Standard Time": [ + "Europe/Sarajevo"/*, + "Europe/Skopje", + "Europe/Warsaw", + "Europe/Zagreb" + */], + "Central Pacific Standard Time": [ + "Pacific/Efate"/*, + "Pacific/Guadalcanal", + "Pacific/Noumea", + "Pacific/Ponape Pacific/Kosrae", + "Antarctica/Macquarie", + "Etc/GMT-11" + */], + "Central Standard Time": [ + "America/Chicago"/*, + "America/Indiana/Knox", + "America/Indiana/Tell_City", + "America/Matamoros", + "America/Menominee", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Resolute", + "America/Winnipeg", + "CST6CDT" + */], + "Central Standard Time (Mexico)": [ + "America/Bahia_Banderas"/*, + "America/Merida", + "America/Mexico_City", + "America/Monterrey" + */], + "Chatham Islands Standard Time": [ + "Pacific/Chatham" + ], + "China Standard Time": [ + "Asia/Hong_Kong"/*, + "Asia/Macau", + "Asia/Shanghai" + */], + "Cuba Standard Time": [ + "America/Havana" + ], + "Dateline Standard Time": [ + "Etc/GMT+12" + ], + "E. Africa Standard Time": [ + "Africa/Addis_Ababa"/*, + "Africa/Asmera", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Juba", + "Africa/Kampala", + "Africa/Mogadishu", + "Africa/Nairobi", + "Indian/Antananarivo", + "Indian/Comoro", + "Indian/Mayotte", + "Antarctica/Syowa", + "Etc/GMT-3" + */], + "E. Australia Standard Time": [ + "Australia/Brisbane"/*, + "Australia/Lindeman" + */], + "E. Europe Standard Time": [ + "Europe/Chisinau" + ], + "E. South America Standard Time": [ + "America/Sao_Paulo" + ], + "Easter Island Standard Time": [ + "Pacific/Easter" + ], + "Eastern Standard Time": [ + "America/Detroit"/*, + "America/Indiana/Petersburg", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Iqaluit", + "America/Kentucky/Monticello", + "America/Louisville", + "America/Montreal", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Pangnirtung", + "America/Thunder_Bay", + "America/Toronto", + "EST5EDT" + */], + "Eastern Standard Time (Mexico)": [ + "America/Cancun" + ], + "Egypt Standard Time": [ + "Africa/Cairo" + ], + "Ekaterinburg Standard Time": [ + "Asia/Yekaterinburg" + ], + "Fiji Standard Time": [ + "Pacific/Fiji" + ], + "FLE Standard Time": [ + "Europe/Helsinki"/*, + "Europe/Kiev", + "Europe/Mariehamn", + "Europe/Riga", + "Europe/Sofia", + "Europe/Tallinn", + "Europe/Uzhgorod", + "Europe/Vilnius", + "Europe/Zaporozhye" + */], + "Georgian Standard Time": [ + "Asia/Tbilisi" + ], + "GMT Standard Time": [ + "Atlantic/Canary"/*, + "Atlantic/Faeroe", + "Atlantic/Madeira", + "Europe/Dublin", + "Europe/Guernsey", + "Europe/Isle_of_Man", + "Europe/Jersey", + "Europe/Lisbon", + "Europe/London" + */], + "Greenland Standard Time": [ + "America/Godthab" + ], + "Greenwich Standard Time": [ + "Africa/Abidjan"/*, + "Africa/Accra", + "Africa/Bamako", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Freetown", + "Africa/Lome", + "Africa/Monrovia", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Atlantic/Reykjavik", + "Atlantic/St_Helena" + */], + "GTB Standard Time": [ + "Asia/Nicosia"/*, + "Asia/Famagusta", + "Europe/Athens", + "Europe/Bucharest" + */], + "Haiti Standard Time": [ + "America/Port-au-Prince" + ], + "Hawaiian Standard Time": [ + "Pacific/Honolulu"/*, + "Pacific/Johnston", + "Pacific/Rarotonga", + "Pacific/Tahiti", + "Etc/GMT+10" + */], + "India Standard Time": [ + "Asia/Calcutta" + ], + "Iran Standard Time": [ + "Asia/Tehran" + ], + "Israel Standard Time": [ + "Asia/Jerusalem" + ], + "Jordan Standard Time": [ + "Asia/Amman" + ], + "Kaliningrad Standard Time": [ + "Europe/Kaliningrad" + ], + "Korea Standard Time": [ + "Asia/Seoul" + ], + "Libya Standard Time": [ + "Africa/Tripoli" + ], + "Line Islands Standard Time": [ + "Pacific/Kiritimati"/*, + "Etc/GMT-14" + */], + "Lord Howe Standard Time": [ + "Australia/Lord_Howe" + ], + "Magadan Standard Time": [ + "Asia/Magadan" + ], + "Magallanes Standard Time": [ + "America/Punta_Arenas" + ], + "Marquesas Standard Time": [ + "Pacific/Marquesas" + ], + "Mauritius Standard Time": [ + "Indian/Mauritius"/*, + "Indian/Mahe", + "Indian/Reunion" + */], + "Middle East Standard Time": [ + "Asia/Beirut" + ], + "Montevideo Standard Time": [ + "America/Montevideo" + ], + "Morocco Standard Time": [ + "Africa/Casablanca"/*, + "Africa/El_Aaiun" + */], + "Mountain Standard Time": [ + "America/Boise"/*, + "America/Cambridge_Bay", + "America/Denver", + "America/Edmonton", + "America/Inuvik", + "America/Ojinaga", + "America/Yellowknife", + "MST7MDT" + */], + "Mountain Standard Time (Mexico)": [ + "America/Chihuahua"/*, + "America/Mazatlan" + */], + "Myanmar Standard Time": [ + "Asia/Rangoon"/*, + "Indian/Cocos" + */], + "N. Central Asia Standard Time": [ + "Asia/Novosibirsk" + ], + "Namibia Standard Time": [ + "Africa/Windhoek" + ], + "Nepal Standard Time": [ + "Asia/Katmandu" + ], + "New Zealand Standard Time": [ + "Pacific/Auckland"/*, + "Antarctica/McMurdo" + */], + "Newfoundland Standard Time": [ + "America/St_Johns" + ], + "Norfolk Standard Time": [ + "Pacific/Norfolk" + ], + "North Asia East Standard Time": [ + "Asia/Irkutsk" + ], + "North Asia Standard Time": [ + "Asia/Krasnoyarsk"/*, + "Asia/Novokuznetsk" + */], + "North Korea Standard Time": [ + "Asia/Pyongyang" + ], + "Omsk Standard Time": [ + "Asia/Omsk" + ], + "Pacific SA Standard Time": [ + "America/Santiago" + ], + "Pacific Standard Time": [ + "America/Los_Angeles"/*, + "America/Dawson", + "America/Vancouver", + "America/Whitehorse", + "PST8PDT" + */], + "Pacific Standard Time (Mexico)": [ + "America/Tijuana"/*, + "America/Santa_Isabel" + */], + "Pakistan Standard Time": [ + "Asia/Karachi" + ], + "Paraguay Standard Time": [ + "America/Asuncion" + ], + "Qyzylorda Standard Time": [ + "Asia/Qyzylorda" + ], + "Romance Standard Time": [ + "Europe/Paris"/*, + "Europe/Brussels", + "Europe/Copenhagen", + "Europe/Madrid", + "Africa/Ceuta" + */], + "Russia Time Zone 3": [ + "Europe/Samara" + ], + "Russia Time Zone 10": [ + "Asia/Srednekolymsk" + ], + "Russia Time Zone 11": [ + "Asia/Kamchatka"/*, + "Asia/Anadyr" + */], + "Russian Standard Time": [ + "Europe/Moscow"/*, + "Europe/Kirov", + "Europe/Simferopol" + */], + "SA Eastern Standard Time": [ + "America/Belem"/*, + "America/Cayenne", + "America/Fortaleza", + "America/Maceio", + "America/Paramaribo", + "America/Recife", + "America/Santarem", + "Atlantic/Stanley", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Etc/GMT+3" + */], + "SA Pacific Standard Time": [ + "America/Bogota"/*, + "America/Cayman", + "America/Coral_Harbour", + "America/Eirunepe", + "America/Guayaquil", + "America/Jamaica", + "America/Lima", + "America/Panama", + "America/Rio_Branco", + "Etc/GMT+5" + */], + "SA Western Standard Time": [ + "America/Anguilla"/*, + "America/Antigua", + "America/Aruba", + "America/Barbados", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Curacao", + "America/Dominica", + "America/Grenada", + "America/Guadeloupe", + "America/Guyana", + "America/Kralendijk", + "America/La_Paz", + "America/Lower_Princes", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Montserrat", + "America/Port_of_Spain", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Santo_Domingo", + "America/St_Barthelemy", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Tortola", + "Etc/GMT+4" + */], + "Saint Pierre Standard Time": [ + "America/Miquelon" + ], + "Sakhalin Standard Time": [ + "Asia/Sakhalin" + ], + "Samoa Standard Time": [ + "Pacific/Apia" + ], + "Sao Tome Standard Time": [ + "Africa/Sao_Tome" + ], + "Saratov Standard Time": [ + "Europe/Saratov" + ], + "SE Asia Standard Time": [ + "Asia/Bangkok"/*, + "Asia/Jakarta", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Saigon", + "Asia/Vientiane", + "Indian/Christmas", + "Antarctica/Davis", + "Etc/GMT-7" + */], + "Singapore Standard Time": [ + "Asia/Singapore"/*, + "Asia/Brunei", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Makassar", + "Asia/Manila", + "Antarctica/Casey", + "Etc/GMT-8" + */], + "South Africa Standard Time": [ + "Africa/Johannesburg"/*, + "Africa/Blantyre", + "Africa/Bujumbura", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Kigali", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Etc/GMT-2" + */], + "Sri Lanka Standard Time": [ + "Asia/Colombo" + ], + "Sudan Standard Time": [ + "Africa/Khartoum" + ], + "Syria Standard Time": [ + "Asia/Damascus" + ], + "Taipei Standard Time": [ + "Asia/Taipei" + ], + "Tasmania Standard Time": [ + "Australia/Currie"/*, + "Australia/Hobart" + */], + "Tocantins Standard Time": [ + "America/Araguaina" + ], + "Tokyo Standard Time": [ + "Asia/Tokyo"/*, + "Asia/Dili", + "Asia/Jayapura", + "Pacific/Palau", + "Etc/GMT-9" + */], + "Tomsk Standard Time": [ + "Asia/Tomsk" + ], + "Tonga Standard Time": [ + "Pacific/Tongatapu" + ], + "Transbaikal Standard Time": [ + "Asia/Chita" + ], + "Turkey Standard Time": [ + "Europe/Istanbul" + ], + "Turks And Caicos Standard Time": [ + "America/Grand_Turk" + ], + "Ulaanbaatar Standard Time": [ + "Asia/Ulaanbaatar"/*, + "Asia/Choibalsan" + */], + "US Eastern Standard Time": [ + "America/Indianapolis"/*, + "America/Indiana/Marengo", + "America/Indiana/Vevay" + */], + "US Mountain Standard Time": [ + "America/Phoenix"/*, + "America/Creston", + "America/Dawson_Creek", + "America/Fort_Nelson", + "America/Hermosillo", + "Etc/GMT+7" + */], + "UTC": [ + "Etc/GMT"/*, + "America/Danmarkshavn", + "Etc/UTC" + */], + "UTC-02": [ + "Etc/GMT+2"/*, + "America/Noronha", + "Atlantic/South_Georgia" + */], + "UTC-08": [ + "Etc/GMT+8"/*, + "Pacific/Pitcairn" + */], + "UTC-09": [ + "Etc/GMT+9"/*, + "Pacific/Gambier" + */], + "UTC-11": [ + "Etc/GMT+11"/*, + "Pacific/Midway", + "Pacific/Niue", + "Pacific/Pago_Pago" + */], + "UTC+12": [ + "Etc/GMT-12"/*, + "Pacific/Funafuti", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Nauru", + "Pacific/Tarawa", + "Pacific/Wake", + "Pacific/Wallis" + */], + "UTC+13": [ + "Etc/GMT-13"/*, + "Pacific/Enderbury", + "Pacific/Fakaofo" + */], + "Venezuela Standard Time": [ + "America/Caracas" + ], + "Vladivostok Standard Time": [ + "Asia/Vladivostok"/*, + "Asia/Ust-Nera" + */], + "Volgograd Standard Time": [ + "Europe/Volgograd" + ], + "W. Australia Standard Time": [ + "Australia/Perth" + ], + "W. Central Africa Standard Time": [ + "Africa/Algiers"/*, + "Africa/Bangui", + "Africa/Brazzaville", + "Africa/Douala", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Luanda", + "Africa/Malabo", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Porto-Novo", + "Africa/Tunis", + "Etc/GMT-1" + */], + "W. Europe Standard Time": [ + "Europe/Amsterdam"/*, + "Europe/Andorra", + "Europe/Berlin", + "Europe/Busingen", + "Europe/Gibraltar", + "Europe/Luxembourg", + "Europe/Malta", + "Europe/Monaco", + "Europe/Oslo", + "Europe/Rome", + "Europe/San_Marino", + "Europe/Stockholm", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Zurich", + "Arctic/Longyearbyen" + */], + "W. Mongolia Standard Time": [ + "Asia/Hovd" + ], + "West Asia Standard Time": [ + "Asia/Aqtau"/*, + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Atyrau", + "Asia/Dushanbe", + "Asia/Oral", + "Asia/Samarkand", + "Asia/Tashkent", + "Indian/Kerguelen", + "Indian/Maldives", + "Antarctica/Mawson", + "Etc/GMT-5" + */], + "West Bank Standard Time": [ + "Asia/Gaza"/*, + "Asia/Hebron" + */], + "West Pacific Standard Time": [ + "Pacific/Guam"/*, + "Pacific/Port_Moresby", + "Pacific/Saipan", + "Pacific/Truk", + "Antarctica/DumontDUrville", + "Etc/GMT-10" + */], + "Yakutsk Standard Time": [ + "Asia/Khandyga"/*, + "Asia/Yakutsk" + */], +}; diff --git a/plugins/white-list/index.php b/plugins/white-list/index.php index efad99ea8..2b91b8bcc 100644 --- a/plugins/white-list/index.php +++ b/plugins/white-list/index.php @@ -4,35 +4,31 @@ class WhiteListPlugin extends \RainLoop\Plugins\AbstractPlugin { const NAME = 'Whitelist', - VERSION = '2.1', - RELEASE = '2021-04-21', + VERSION = '2.2', + RELEASE = '2024-03-04', REQUIRED = '2.5.0', CATEGORY = 'Login', DESCRIPTION = 'Simple login whitelist (with wildcard and exceptions functionality).'; public function Init() : void { - $this->addHook('login.credentials', 'FilterLoginCredentials'); + $this->addHook('login.credentials.step-1', 'FilterLoginCredentials'); } /** - * @param string $sEmail - * @param string $sLogin - * @param string $sPassword - * * @throws \RainLoop\Exceptions\ClientException */ - public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword) + public function FilterLoginCredentials(string &$sEmail) { $sWhiteList = \trim($this->Config()->Get('plugin', 'white_list', '')); - if (0 < strlen($sWhiteList) && !\RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sWhiteList)) - { + if (\strlen($sWhiteList) && !\RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sWhiteList)) { $sExceptions = \trim($this->Config()->Get('plugin', 'exceptions', '')); - if (0 === \strlen($sExceptions) || !\RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sExceptions)) - { + if (!\strlen($sExceptions) || \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sExceptions)) { throw new \RainLoop\Exceptions\ClientException( - $this->Config()->Get('plugin', 'auth_error', true) ? - \RainLoop\Notifications::AuthError : \RainLoop\Notifications::AccountNotAllowed); + $this->Config()->Get('plugin', 'auth_error', false) + ? \RainLoop\Notifications::AuthError + : \RainLoop\Notifications::AccountNotAllowed + ); } } } @@ -46,7 +42,7 @@ class WhiteListPlugin extends \RainLoop\Plugins\AbstractPlugin \RainLoop\Plugins\Property::NewInstance('auth_error')->SetLabel('Auth Error') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetDescription('Throw an authentication error instead of an access error.') - ->SetDefaultValue(true), + ->SetDefaultValue(false), \RainLoop\Plugins\Property::NewInstance('white_list')->SetLabel('White List') ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) ->SetDescription('Emails white list, space as delimiter, wildcard supported.') diff --git a/plugins/wysiwyg-example/example.js b/plugins/wysiwyg-example/example.js new file mode 100644 index 000000000..a26fe5158 --- /dev/null +++ b/plugins/wysiwyg-example/example.js @@ -0,0 +1,82 @@ +(rl => { + class Example + { + constructor(owner, editor) { + this.mode = 'wysiwyg'; + this.owner = owner; + this.editor = editor; +console.dir({editor}); + } + + setMode(mode) { + console.log(`WYSIWYG-Example.setMode(${mode})`); + this.mode = mode; + } + + on(type, fn) { + console.log(`WYSIWYG-Example.on(${type}, ${fn})`); + } + + execCommand(cmd, cfg) { + console.log(`WYSIWYG-Example.execCommand(${cmd}, ${cfg})`); +/* + execCommand('insertSignature', { + clearCache: true + })); + + execCommand('insertSignature', { + isHtml: html, + insertBefore: insertBefore, + signature: signature + })); +*/ + } + + getData() { + console.log(`WYSIWYG-Example.getData()`); + return this.editor.innerHTML; + } + + setData(html) { + console.log(`WYSIWYG-Example.setData(${html})`); + this.editor.innerHTML = html; + } + + getPlainData() { + console.log(`WYSIWYG-Example.getPlainData()`); + return this.editor.innerText; + } + + setPlainData(text) { + console.log(`WYSIWYG-Example.setPlainData(${text})`); + return this.editor.textContent = text; + } + + blur() { + console.log(`WYSIWYG-Example.blur()`); + this.editor.blur(); + } + + focus() { + console.log(`WYSIWYG-Example.focus()`); + } + } + + if (rl) { + const path = rl.settings.app('webVersionPath'), + script = document.createElement('script'); + script.src = path + 'static/wysiwyg-example/example.min.js'; + document.head.append(script); + + /** + * owner = HtmlEditor + * container = HTMLElement + * onReady = callback(SMQuill) + */ + rl.registerWYSIWYG('Example', (owner, container, onReady) => { + const editor = new Example(owner, container); + onReady(editor); + }); + } + +})(window.rl); diff --git a/plugins/wysiwyg-example/index.php b/plugins/wysiwyg-example/index.php new file mode 100644 index 000000000..c52d411f3 --- /dev/null +++ b/plugins/wysiwyg-example/index.php @@ -0,0 +1,41 @@ +isDir()) { + \mkdir($path . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } else { + \copy($item, $path . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } + } + umask($old_mask); + } + + if (\is_file("{$path}/example.min.js")) { +// $this->addCss('style.css'); + $this->addJs('example.js'); + } + } +} diff --git a/plugins/wysiwyg-example/static/example.min.js b/plugins/wysiwyg-example/static/example.min.js new file mode 100644 index 000000000..8d1c8b69c --- /dev/null +++ b/plugins/wysiwyg-example/static/example.min.js @@ -0,0 +1 @@ + diff --git a/public_html/translate.php b/public_html/translate.php new file mode 100644 index 000000000..30f6613ea --- /dev/null +++ b/public_html/translate.php @@ -0,0 +1,197 @@ + $values) { + if (is_array($values)) { + foreach ($values as $key => $value) { + $data[$section][$key] = preg_replace('/\\R/', "\n", trim($value)); + } + } else if ('LANG_DIR' === $section) { + $data[$section] = $values; + } + } + return str_replace(' ', "\t", json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); +} + +if ('POST' === $_SERVER['REQUEST_METHOD']) { + try { + $file = tempnam(sys_get_temp_dir(), ''); + $zip = new ZipArchive(); + if (!$zip->open($file, ZIPARCHIVE::CREATE)) { + exit("Failed to create zip"); + } + if (!$lang) { + $lang = (empty($_POST['lang']) || !preg_match('/^[a-z]{2}(-[A-Z]{2})?$/D',$_POST['lang'])) + ? 'new' : $_POST['lang']; + } + $zip->addFromString("{$lang}/admin.json", toJSON($_POST['admin'])); + $zip->addFromString("{$lang}/user.json", toJSON($_POST['user'])); + $zip->close(); + header('Content-Type: application/zip'); + header('Content-disposition: attachment; filename="snappymail-'.$lang.'.zip"'); + header('Content-Length: ' . filesize($file)); + readfile($file); + } catch (\Throwable $e) { + echo $e->getMessage(); + } + unlink($file); + exit; +} + +// /home/rainloop/public_html/snappymail/v/0.0.0/static/js +$_ENV['SNAPPYMAIL_INCLUDE_AS_API'] = true; +require 'demo/index.php'; + +$root = APP_VERSION_ROOT_PATH . 'app/localization'; + +$en = [ + 'user' => '', + 'admin' => '', +// 'static' => '', +]; +foreach ($en as $name => $data) { + $en[$name] = json_decode(file_get_contents("{$root}/en/{$name}.json"), true); +} + +$languages = ['']; +foreach (glob("{$root}/*", GLOB_ONLYDIR) as $dir) { + $name = basename($dir); + if ('en' !== $name) { + $languages[$name] = ""; + } +} +ksort($languages); + +$lang_names = json_decode(file_get_contents("{$root}/langs.json"), true)['LANGS_NAMES_EN']; + +$other_langs = []; +foreach ($lang_names as $key => $name) { + if ('en' !== $key && !isset($languages[$key])) { + $other_langs[$key] = ""; + } +} + +//print_r($languages); + +echo ' + + + + Translate + + +

Translate:

+
+ Show untranslated only + + + + + + +'; +foreach ($en as $name => $sections) { + echo ''; + $data = $sections; + if ($lang && is_readable("{$root}/{$lang}/{$name}.json")) { + $data = json_decode(file_get_contents("{$root}/{$lang}/{$name}.json"), true); + } + foreach ($sections as $section => $values) { + if (is_array($values)) { + echo ''; + foreach ($values as $key => $value) { + echo ''; +// echo ''; + echo ''; + echo ''; + echo ''; + } + } else if ('LANG_DIR' === $section) { + echo ''; + echo ''; + echo ''; + echo ''; + } + } + echo ''; +} +echo '
en'.($lang ? "{$lang_names[$lang]} ({$lang})" : '').'
'.$name.'
'.$section.'
'.$section.'/'.$key.''.htmlspecialchars($value).'
Text direction
'; +echo '
'; + +/* + + diff --git a/snappymail/v/0.0.0/app/templates/Views/Common/Paginator.html b/snappymail/v/0.0.0/app/templates/Views/Common/Paginator.html index 6295e4fd0..5d8177992 100644 --- a/snappymail/v/0.0.0/app/templates/Views/Common/Paginator.html +++ b/snappymail/v/0.0.0/app/templates/Views/Common/Paginator.html @@ -1,3 +1,3 @@ diff --git a/snappymail/v/0.0.0/app/templates/Views/User/Login.html b/snappymail/v/0.0.0/app/templates/Views/User/Login.html index aed43eaa0..e1dfbbf3d 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/Login.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/Login.html @@ -7,7 +7,7 @@

-
@@ -25,7 +25,7 @@
-
+
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailFolderList.html b/snappymail/v/0.0.0/app/templates/Views/User/MailFolderList.html index 2c7147e5f..3103049a1 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailFolderList.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailFolderList.html @@ -1,4 +1,4 @@ -
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailFolderListItem.html b/snappymail/v/0.0.0/app/templates/Views/User/MailFolderListItem.html index b433f86e7..35bb12f22 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailFolderListItem.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailFolderListItem.html @@ -1,13 +1,11 @@ -
  • + css: { 'selected': selected() && !isSystemFolder(), 'system': isSystemFolder, 'selectable': canBeSelected, 'unread-sub': hasUnreadInSub, 'anim-action-class': actionBlink }, + attr: { 'data-unread': unreadCount, href: href }"> - -
      + +
      • - diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html index 4a4dd3701..9bc226a08 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageList.html @@ -17,8 +17,11 @@ 🗑 +
        -